capybara 3.12.0 → 3.13.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +13 -0
  3. data/README.md +13 -3
  4. data/lib/capybara.rb +8 -4
  5. data/lib/capybara/config.rb +3 -1
  6. data/lib/capybara/driver/base.rb +2 -2
  7. data/lib/capybara/driver/node.rb +11 -2
  8. data/lib/capybara/minitest.rb +3 -3
  9. data/lib/capybara/minitest/spec.rb +10 -3
  10. data/lib/capybara/node/actions.rb +4 -0
  11. data/lib/capybara/node/base.rb +13 -5
  12. data/lib/capybara/node/document.rb +12 -0
  13. data/lib/capybara/node/element.rb +37 -0
  14. data/lib/capybara/node/finders.rb +1 -0
  15. data/lib/capybara/node/matchers.rb +19 -4
  16. data/lib/capybara/node/simple.rb +7 -2
  17. data/lib/capybara/queries/selector_query.rb +93 -9
  18. data/lib/capybara/rspec/matchers.rb +11 -3
  19. data/lib/capybara/rspec/matchers/become_closed.rb +1 -1
  20. data/lib/capybara/rspec/matchers/match_style.rb +38 -0
  21. data/lib/capybara/selector.rb +30 -30
  22. data/lib/capybara/selector/selector.rb +47 -3
  23. data/lib/capybara/selenium/driver.rb +12 -11
  24. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +3 -3
  25. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +2 -2
  26. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +13 -0
  27. data/lib/capybara/selenium/extensions/find.rb +81 -0
  28. data/lib/capybara/selenium/extensions/scroll.rb +78 -0
  29. data/lib/capybara/selenium/node.rb +52 -28
  30. data/lib/capybara/session.rb +13 -1
  31. data/lib/capybara/spec/public/test.js +1 -0
  32. data/lib/capybara/spec/session/assert_style_spec.rb +4 -4
  33. data/lib/capybara/spec/session/attach_file_spec.rb +6 -6
  34. data/lib/capybara/spec/session/click_button_spec.rb +1 -1
  35. data/lib/capybara/spec/session/evaluate_script_spec.rb +1 -0
  36. data/lib/capybara/spec/session/execute_script_spec.rb +1 -0
  37. data/lib/capybara/spec/session/fill_in_spec.rb +7 -1
  38. data/lib/capybara/spec/session/find_field_spec.rb +1 -1
  39. data/lib/capybara/spec/session/find_spec.rb +11 -0
  40. data/lib/capybara/spec/session/frame/switch_to_frame_spec.rb +0 -1
  41. data/lib/capybara/spec/session/has_css_spec.rb +32 -0
  42. data/lib/capybara/spec/session/has_select_spec.rb +2 -2
  43. data/lib/capybara/spec/session/has_selector_spec.rb +7 -0
  44. data/lib/capybara/spec/session/has_text_spec.rb +1 -1
  45. data/lib/capybara/spec/session/matches_style_spec.rb +35 -0
  46. data/lib/capybara/spec/session/scroll_spec.rb +117 -0
  47. data/lib/capybara/spec/session/select_spec.rb +5 -0
  48. data/lib/capybara/spec/spec_helper.rb +1 -0
  49. data/lib/capybara/spec/views/obscured.erb +1 -1
  50. data/lib/capybara/spec/views/scroll.erb +20 -0
  51. data/lib/capybara/spec/views/with_html.erb +1 -1
  52. data/lib/capybara/version.rb +1 -1
  53. data/lib/capybara/window.rb +1 -1
  54. data/spec/basic_node_spec.rb +11 -0
  55. data/spec/dsl_spec.rb +1 -1
  56. data/spec/minitest_spec.rb +2 -2
  57. data/spec/minitest_spec_spec.rb +1 -1
  58. data/spec/rack_test_spec.rb +1 -0
  59. data/spec/result_spec.rb +2 -2
  60. data/spec/selector_spec.rb +11 -1
  61. data/spec/selenium_spec_firefox.rb +1 -1
  62. data/spec/selenium_spec_ie.rb +70 -9
  63. data/spec/session_spec.rb +9 -0
  64. data/spec/shared_selenium_session.rb +4 -3
  65. data/spec/spec_helper.rb +2 -0
  66. metadata +38 -6
  67. data/lib/capybara/rspec/matchers/have_style.rb +0 -23
  68. data/lib/capybara/spec/session/has_style_spec.rb +0 -25
@@ -4,7 +4,7 @@ module Capybara
4
4
  module Queries
5
5
  class SelectorQuery < Queries::BaseQuery
6
6
  attr_reader :expression, :selector, :locator, :options
7
- VALID_KEYS = COUNT_KEYS + %i[text id class visible exact exact_text normalize_ws match wait filter_set]
7
+ VALID_KEYS = COUNT_KEYS + %i[text id class style visible exact exact_text normalize_ws match wait filter_set]
8
8
  VALID_MATCH = %i[first smart prefer_exact one].freeze
9
9
 
10
10
  def initialize(*args,
@@ -14,6 +14,7 @@ module Capybara
14
14
  **options,
15
15
  &filter_block)
16
16
  @resolved_node = nil
17
+ @resolved_count = 0
17
18
  @options = options.dup
18
19
  super(@options)
19
20
  self.session_options = session_options
@@ -50,9 +51,24 @@ module Capybara
50
51
  end
51
52
  desc << " with id #{options[:id]}" if options[:id]
52
53
  desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
54
+ desc << case options[:style]
55
+ when String
56
+ " with style attribute #{options[:style].inspect}"
57
+ when Regexp
58
+ " with style attribute matching #{options[:style].inspect}"
59
+ when Hash
60
+ " with styles #{options[:style].inspect}"
61
+ else ''
62
+ end
53
63
  desc << selector.description(node_filters: show_for[:node], **options)
54
64
  desc << ' that also matches the custom filter block' if @filter_block && show_for[:node]
55
65
  desc << " within #{@resolved_node.inspect}" if describe_within?
66
+ if locator.is_a?(String) && locator.start_with?('#', './/', '//')
67
+ unless selector.raw_locator?
68
+ desc << "\nNote: It appears you may be passing a CSS selector or XPath expression rather than a locator. " \
69
+ "Please see the documentation for acceptable locator values.\n\n"
70
+ end
71
+ end
56
72
  desc
57
73
  end
58
74
 
@@ -91,7 +107,9 @@ module Capybara
91
107
  exact = exact? if exact.nil?
92
108
  expr = apply_expression_filters(@expression)
93
109
  expr = exact ? expr.to_xpath(:exact) : expr.to_s if expr.respond_to?(:to_xpath)
94
- filtered_expression(expr)
110
+ expr = filtered_expression(expr)
111
+ expr = "(#{expr})[#{xpath_text_conditions}]" if try_text_match_in_expression?
112
+ expr
95
113
  end
96
114
 
97
115
  def css
@@ -102,6 +120,7 @@ module Capybara
102
120
  def resolve_for(node, exact = nil)
103
121
  applied_filters.clear
104
122
  @resolved_node = node
123
+ @resolved_count += 1
105
124
  node.synchronize do
106
125
  children = find_nodes_by_selector_format(node, exact).map(&method(:to_element))
107
126
  Capybara::Result.new(children, self)
@@ -123,6 +142,26 @@ module Capybara
123
142
 
124
143
  private
125
144
 
145
+ def text_fragments
146
+ text = (options[:text] || options[:exact_text])
147
+ text.is_a?(String) ? text.split : []
148
+ end
149
+
150
+ def xpath_text_conditions
151
+ (options[:text] || options[:exact_text]).split.map { |txt| XPath.contains(txt) }.reduce(&:&)
152
+ end
153
+
154
+ def try_text_match_in_expression?
155
+ first_try? &&
156
+ (options[:text] || options[:exact_text]).is_a?(String) &&
157
+ @resolved_node&.respond_to?(:session) &&
158
+ @resolved_node.session.driver.wait?
159
+ end
160
+
161
+ def first_try?
162
+ @resolved_count == 1
163
+ end
164
+
126
165
  def show_for_stage(only_applied)
127
166
  lambda do |stage = :any|
128
167
  !only_applied || (stage == :any ? applied_filters.any? : applied_filters.include?(stage))
@@ -141,10 +180,25 @@ module Capybara
141
180
  end
142
181
 
143
182
  def find_nodes_by_selector_format(node, exact)
183
+ hints = {}
184
+ hints[:uses_visibility] = true unless visible == :all
185
+ hints[:texts] = text_fragments unless selector.format == :xpath
186
+ hints[:styles] = options[:style] if use_default_style_filter?
187
+
144
188
  if selector.format == :css
145
- node.find_css(css)
189
+ if node.method(:find_css).arity != 1
190
+ node.find_css(css, **hints)
191
+ else
192
+ node.find_css(css)
193
+ end
194
+ elsif selector.format == :xpath
195
+ if node.method(:find_xpath).arity != 1
196
+ node.find_xpath(xpath(exact), **hints)
197
+ else
198
+ node.find_xpath(xpath(exact))
199
+ end
146
200
  else
147
- node.find_xpath(xpath(exact))
201
+ raise ArgumentError, "Unknown format: #{selector.format}"
148
202
  end
149
203
  end
150
204
 
@@ -237,6 +291,7 @@ module Capybara
237
291
  conditions = {}
238
292
  conditions[:id] = options[:id] if use_default_id_filter?
239
293
  conditions[:class] = options[:class] if use_default_class_filter?
294
+ conditions[:style] = options[:style] if use_default_style_filter? && !options[:style].is_a?(Hash)
240
295
  builder(expr).add_attribute_conditions(conditions)
241
296
  end
242
297
 
@@ -248,6 +303,10 @@ module Capybara
248
303
  options.key?(:class) && !custom_keys.include?(:class)
249
304
  end
250
305
 
306
+ def use_default_style_filter?
307
+ options.key?(:style) && !custom_keys.include?(:style)
308
+ end
309
+
251
310
  def apply_expression_filters(expression)
252
311
  unapplied_options = options.keys - valid_keys
253
312
  expression_filters.inject(expression) do |expr, (name, ef)|
@@ -298,11 +357,12 @@ module Capybara
298
357
  def matches_system_filters?(node)
299
358
  applied_filters << :system
300
359
 
301
- matches_id_filter?(node) &&
360
+ matches_visible_filter?(node) &&
361
+ matches_id_filter?(node) &&
302
362
  matches_class_filter?(node) &&
363
+ matches_style_filter?(node) &&
303
364
  matches_text_filter?(node) &&
304
- matches_exact_text_filter?(node) &&
305
- matches_visible_filter?(node)
365
+ matches_exact_text_filter?(node)
306
366
  end
307
367
 
308
368
  def matches_id_filter?(node)
@@ -317,6 +377,28 @@ module Capybara
317
377
  node[:class] =~ options[:class]
318
378
  end
319
379
 
380
+ def matches_style_filter?(node)
381
+ case options[:style]
382
+ when String, nil
383
+ true
384
+ when Regexp
385
+ node[:style] =~ options[:style]
386
+ when Hash
387
+ matches_style?(node, options[:style])
388
+ end
389
+ end
390
+
391
+ def matches_style?(node, styles)
392
+ @actual_styles = node.initial_cache[:style] || node.style(*styles.keys)
393
+ styles.all? do |style, value|
394
+ if value.is_a? Regexp
395
+ @actual_styles[style.to_s] =~ value
396
+ else
397
+ @actual_styles[style.to_s] == value
398
+ end
399
+ end
400
+ end
401
+
320
402
  def matches_text_filter?(node)
321
403
  value = options[:text]
322
404
  return true unless value
@@ -334,8 +416,10 @@ module Capybara
334
416
 
335
417
  def matches_visible_filter?(node)
336
418
  case visible
337
- when :visible then node.visible?
338
- when :hidden then !node.visible?
419
+ when :visible then
420
+ node.initial_cache[:visible] || (node.initial_cache[:visible].nil? && node.visible?)
421
+ when :hidden then
422
+ (node.initial_cache[:visible] == false) || (node.initial_cache[:visbile].nil? && !node.visible?)
339
423
  else true
340
424
  end
341
425
  end
@@ -3,7 +3,7 @@
3
3
  require 'capybara/rspec/matchers/have_selector'
4
4
  require 'capybara/rspec/matchers/match_selector'
5
5
  require 'capybara/rspec/matchers/have_current_path'
6
- require 'capybara/rspec/matchers/have_style'
6
+ require 'capybara/rspec/matchers/match_style'
7
7
  require 'capybara/rspec/matchers/have_text'
8
8
  require 'capybara/rspec/matchers/have_title'
9
9
  require 'capybara/rspec/matchers/become_closed'
@@ -124,9 +124,17 @@ module Capybara
124
124
  end
125
125
 
126
126
  # RSpec matcher for element style
127
- # See {Capybara::Node::Matchers#has_style?}
127
+ # See {Capybara::Node::Matchers#matches_style?}
128
+ def match_style(styles, **options)
129
+ Matchers::MatchStyle.new(styles, options)
130
+ end
131
+
132
+ ##
133
+ # @deprecated
134
+ #
128
135
  def have_style(styles, **options)
129
- Matchers::HaveStyle.new(styles, options)
136
+ warn 'DEPRECATED: have_style is deprecated, please use match_style'
137
+ match_style(styles, **options)
130
138
  end
131
139
 
132
140
  %w[selector css xpath text title current_path link button
@@ -15,7 +15,7 @@ module Capybara
15
15
  while window.exists?
16
16
  return false if timer.expired?
17
17
 
18
- sleep 0.05
18
+ sleep 0.01
19
19
  end
20
20
  true
21
21
  end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/rspec/matchers/base'
4
+
5
+ module Capybara
6
+ module RSpecMatchers
7
+ module Matchers
8
+ class MatchStyle < WrappedElementMatcher
9
+ def element_matches?(el)
10
+ el.assert_matches_style(*@args)
11
+ end
12
+
13
+ def does_not_match?(_actual)
14
+ raise ArgumentError, 'The match_style matcher does not support use with not_to/should_not'
15
+ end
16
+
17
+ def description
18
+ 'match style'
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+
25
+ module Capybara
26
+ module RSpecMatchers
27
+ module Matchers
28
+ ##
29
+ # @deprecated
30
+ class HaveStyle < MatchStyle
31
+ def initialize(*args, &filter_block)
32
+ warn 'HaveStyle matcher is deprecated, please use the MatchStyle matcher instead'
33
+ super
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -25,24 +25,24 @@ end
25
25
 
26
26
  # rubocop:disable Metrics/BlockLength
27
27
 
28
- Capybara.add_selector(:xpath) do
28
+ Capybara.add_selector(:xpath, locator_type: [:to_xpath, String], raw_locator: true) do
29
29
  xpath { |xpath| xpath }
30
30
  end
31
31
 
32
- Capybara.add_selector(:css) do
32
+ Capybara.add_selector(:css, locator_type: [String, Symbol], raw_locator: true) do
33
33
  css { |css| css }
34
34
  end
35
35
 
36
- Capybara.add_selector(:id) do
36
+ Capybara.add_selector(:id, locator_type: [String, Symbol, Regexp]) do
37
37
  xpath { |id| builder(XPath.descendant).add_attribute_conditions(id: id) }
38
38
  locator_filter { |node, id| id.is_a?(Regexp) ? node[:id] =~ id : true }
39
39
  end
40
40
 
41
- Capybara.add_selector(:field) do
42
- visible { |options| :hidden if options[:type] == 'hidden' }
41
+ Capybara.add_selector(:field, locator_type: [String, Symbol]) do
42
+ visible { |options| :hidden if options[:type].to_s == 'hidden' }
43
43
  xpath do |locator, **options|
44
44
  invalid_types = %w[submit image]
45
- invalid_types << 'hidden' unless options[:type] == 'hidden'
45
+ invalid_types << 'hidden' unless options[:type].to_s == 'hidden'
46
46
  xpath = XPath.descendant(:input, :textarea, :select)[!XPath.attr(:type).one_of(*invalid_types)]
47
47
  locate_field(xpath, locator, options)
48
48
  end
@@ -78,7 +78,7 @@ Capybara.add_selector(:field) do
78
78
  end
79
79
  end
80
80
 
81
- Capybara.add_selector(:fieldset) do
81
+ Capybara.add_selector(:fieldset, locator_type: [String, Symbol]) do
82
82
  xpath do |locator, legend: nil, **|
83
83
  locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]
84
84
  locator_matchers |= XPath.attr(test_id) == locator.to_s if test_id
@@ -90,7 +90,7 @@ Capybara.add_selector(:fieldset) do
90
90
  node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
91
91
  end
92
92
 
93
- Capybara.add_selector(:link) do
93
+ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
94
94
  xpath do |locator, href: true, alt: nil, title: nil, **|
95
95
  xpath = builder(XPath.descendant(:a)).add_attribute_conditions(href: href)
96
96
 
@@ -132,7 +132,7 @@ Capybara.add_selector(:link) do
132
132
  end
133
133
  end
134
134
 
135
- Capybara.add_selector(:button) do
135
+ Capybara.add_selector(:button, locator_type: [String, Symbol]) do
136
136
  xpath(:value, :title, :type) do |locator, **options|
137
137
  input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
138
138
  btn_xpath = XPath.descendant(:button)
@@ -166,7 +166,7 @@ Capybara.add_selector(:button) do
166
166
  end
167
167
  end
168
168
 
169
- Capybara.add_selector(:link_or_button) do
169
+ Capybara.add_selector(:link_or_button, locator_type: [String, Symbol]) do
170
170
  label 'link or button'
171
171
  xpath do |locator, **options|
172
172
  self.class.all.values_at(:link, :button).map do |selector|
@@ -181,9 +181,8 @@ Capybara.add_selector(:link_or_button) do
181
181
  end
182
182
  end
183
183
 
184
- Capybara.add_selector(:fillable_field) do
184
+ Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
185
185
  label 'field'
186
-
187
186
  xpath do |locator, allow_self: nil, **options|
188
187
  xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input, :textarea)[
189
188
  !XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')
@@ -215,9 +214,8 @@ Capybara.add_selector(:fillable_field) do
215
214
  end
216
215
  end
217
216
 
218
- Capybara.add_selector(:radio_button) do
217
+ Capybara.add_selector(:radio_button, locator_type: [String, Symbol]) do
219
218
  label 'radio button'
220
-
221
219
  xpath do |locator, allow_self: nil, **options|
222
220
  xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
223
221
  XPath.attr(:type) == 'radio'
@@ -240,7 +238,7 @@ Capybara.add_selector(:radio_button) do
240
238
  end
241
239
  end
242
240
 
243
- Capybara.add_selector(:checkbox) do
241
+ Capybara.add_selector(:checkbox, locator_type: [String, Symbol]) do
244
242
  xpath do |locator, allow_self: nil, **options|
245
243
  xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
246
244
  XPath.attr(:type) == 'checkbox'
@@ -263,7 +261,7 @@ Capybara.add_selector(:checkbox) do
263
261
  end
264
262
  end
265
263
 
266
- Capybara.add_selector(:select) do
264
+ Capybara.add_selector(:select, locator_type: [String, Symbol]) do
267
265
  label 'select box'
268
266
 
269
267
  xpath do |locator, **options|
@@ -320,7 +318,7 @@ Capybara.add_selector(:select) do
320
318
  end
321
319
  end
322
320
 
323
- Capybara.add_selector(:datalist_input) do
321
+ Capybara.add_selector(:datalist_input, locator_type: [String, Symbol]) do
324
322
  label 'input box with datalist completion'
325
323
 
326
324
  xpath do |locator, **options|
@@ -355,7 +353,7 @@ Capybara.add_selector(:datalist_input) do
355
353
  end
356
354
  end
357
355
 
358
- Capybara.add_selector(:option) do
356
+ Capybara.add_selector(:option, locator_type: [String, Symbol]) do
359
357
  xpath do |locator|
360
358
  xpath = XPath.descendant(:option)
361
359
  xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil?
@@ -373,7 +371,7 @@ Capybara.add_selector(:option) do
373
371
  end
374
372
  end
375
373
 
376
- Capybara.add_selector(:datalist_option) do
374
+ Capybara.add_selector(:datalist_option, locator_type: [String, Symbol]) do
377
375
  label 'datalist option'
378
376
  visible(:all)
379
377
 
@@ -390,7 +388,7 @@ Capybara.add_selector(:datalist_option) do
390
388
  end
391
389
  end
392
390
 
393
- Capybara.add_selector(:file_field) do
391
+ Capybara.add_selector(:file_field, locator_type: [String, Symbol]) do
394
392
  label 'file field'
395
393
  xpath do |locator, allow_self: nil, **options|
396
394
  xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
@@ -404,7 +402,7 @@ Capybara.add_selector(:file_field) do
404
402
  describe_expression_filters
405
403
  end
406
404
 
407
- Capybara.add_selector(:label) do
405
+ Capybara.add_selector(:label, locator_type: [String, Symbol]) do
408
406
  label 'label'
409
407
  xpath(:for) do |locator, options|
410
408
  xpath = XPath.descendant(:label)
@@ -413,12 +411,14 @@ Capybara.add_selector(:label) do
413
411
  locator_matchers |= XPath.attr(test_id) == locator if test_id
414
412
  xpath = xpath[locator_matchers]
415
413
  end
416
- if options.key?(:for) && !options[:for].is_a?(Capybara::Node::Element)
417
- with_attr = XPath.attr(:for) == options[:for].to_s
418
- labelable_elements = %i[button input keygen meter output progress select textarea]
419
- wrapped = !XPath.attr(:for) &
420
- XPath.descendant(*labelable_elements)[XPath.attr(:id) == options[:for].to_s]
421
- xpath = xpath[with_attr | wrapped]
414
+ if options.key?(:for)
415
+ if (for_option = options[:for].is_a?(Capybara::Node::Element) ? options[:for][:id] : options[:for])
416
+ with_attr = XPath.attr(:for) == for_option.to_s
417
+ labelable_elements = %i[button input keygen meter output progress select textarea]
418
+ wrapped = !XPath.attr(:for) &
419
+ XPath.descendant(*labelable_elements)[XPath.attr(:id) == for_option.to_s]
420
+ xpath = xpath[with_attr | wrapped]
421
+ end
422
422
  end
423
423
  xpath
424
424
  end
@@ -442,7 +442,7 @@ Capybara.add_selector(:label) do
442
442
  end
443
443
  end
444
444
 
445
- Capybara.add_selector(:table) do
445
+ Capybara.add_selector(:table, locator_type: [String, Symbol]) do
446
446
  xpath do |locator, caption: nil, **|
447
447
  xpath = XPath.descendant(:table)
448
448
  unless locator.nil?
@@ -459,7 +459,7 @@ Capybara.add_selector(:table) do
459
459
  end
460
460
  end
461
461
 
462
- Capybara.add_selector(:frame) do
462
+ Capybara.add_selector(:frame, locator_type: [String, Symbol]) do
463
463
  xpath do |locator, name: nil, **|
464
464
  xpath = XPath.descendant(:iframe).union(XPath.descendant(:frame))
465
465
  unless locator.nil?
@@ -475,7 +475,7 @@ Capybara.add_selector(:frame) do
475
475
  end
476
476
  end
477
477
 
478
- Capybara.add_selector(:element) do
478
+ Capybara.add_selector(:element, locator_type: [String, Symbol]) do
479
479
  xpath do |locator, **|
480
480
  XPath.descendant.where(locator ? XPath.local_name == locator.to_s : nil)
481
481
  end