capybara 3.18.0 → 3.19.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +16 -0
  3. data/README.md +14 -44
  4. data/lib/capybara/node/actions.rb +2 -2
  5. data/lib/capybara/node/element.rb +3 -5
  6. data/lib/capybara/queries/selector_query.rb +30 -11
  7. data/lib/capybara/rack_test/node.rb +1 -1
  8. data/lib/capybara/result.rb +2 -0
  9. data/lib/capybara/rspec/matcher_proxies.rb +2 -0
  10. data/lib/capybara/rspec/matchers/base.rb +2 -2
  11. data/lib/capybara/rspec/matchers/count_sugar.rb +36 -0
  12. data/lib/capybara/rspec/matchers/have_selector.rb +3 -0
  13. data/lib/capybara/rspec/matchers/have_text.rb +3 -0
  14. data/lib/capybara/selector.rb +196 -599
  15. data/lib/capybara/selector/css.rb +2 -0
  16. data/lib/capybara/selector/definition.rb +276 -0
  17. data/lib/capybara/selector/definition/button.rb +46 -0
  18. data/lib/capybara/selector/definition/checkbox.rb +23 -0
  19. data/lib/capybara/selector/definition/css.rb +5 -0
  20. data/lib/capybara/selector/definition/datalist_input.rb +35 -0
  21. data/lib/capybara/selector/definition/datalist_option.rb +25 -0
  22. data/lib/capybara/selector/definition/element.rb +27 -0
  23. data/lib/capybara/selector/definition/field.rb +40 -0
  24. data/lib/capybara/selector/definition/fieldset.rb +14 -0
  25. data/lib/capybara/selector/definition/file_field.rb +13 -0
  26. data/lib/capybara/selector/definition/fillable_field.rb +33 -0
  27. data/lib/capybara/selector/definition/frame.rb +17 -0
  28. data/lib/capybara/selector/definition/id.rb +6 -0
  29. data/lib/capybara/selector/definition/label.rb +43 -0
  30. data/lib/capybara/selector/definition/link.rb +45 -0
  31. data/lib/capybara/selector/definition/link_or_button.rb +16 -0
  32. data/lib/capybara/selector/definition/option.rb +27 -0
  33. data/lib/capybara/selector/definition/radio_button.rb +24 -0
  34. data/lib/capybara/selector/definition/select.rb +62 -0
  35. data/lib/capybara/selector/definition/table.rb +106 -0
  36. data/lib/capybara/selector/definition/table_row.rb +21 -0
  37. data/lib/capybara/selector/definition/xpath.rb +5 -0
  38. data/lib/capybara/selector/filters/base.rb +4 -0
  39. data/lib/capybara/selector/filters/locator_filter.rb +12 -2
  40. data/lib/capybara/selector/selector.rb +40 -452
  41. data/lib/capybara/selenium/driver.rb +4 -10
  42. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +3 -9
  43. data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +8 -0
  44. data/lib/capybara/selenium/extensions/find.rb +1 -1
  45. data/lib/capybara/selenium/logger_suppressor.rb +5 -0
  46. data/lib/capybara/selenium/node.rb +19 -13
  47. data/lib/capybara/selenium/nodes/chrome_node.rb +30 -0
  48. data/lib/capybara/selenium/nodes/firefox_node.rb +14 -12
  49. data/lib/capybara/selenium/nodes/ie_node.rb +11 -0
  50. data/lib/capybara/selenium/nodes/safari_node.rb +7 -12
  51. data/lib/capybara/server/checker.rb +7 -3
  52. data/lib/capybara/session.rb +2 -2
  53. data/lib/capybara/spec/session/all_spec.rb +1 -1
  54. data/lib/capybara/spec/session/find_spec.rb +1 -1
  55. data/lib/capybara/spec/session/first_spec.rb +1 -1
  56. data/lib/capybara/spec/session/has_css_spec.rb +7 -0
  57. data/lib/capybara/spec/session/has_text_spec.rb +6 -0
  58. data/lib/capybara/spec/session/save_screenshot_spec.rb +11 -0
  59. data/lib/capybara/spec/session/select_spec.rb +0 -5
  60. data/lib/capybara/spec/test_app.rb +8 -3
  61. data/lib/capybara/version.rb +1 -1
  62. data/lib/capybara/window.rb +1 -1
  63. data/spec/minitest_spec_spec.rb +1 -0
  64. data/spec/selector_spec.rb +12 -6
  65. data/spec/selenium_spec_firefox.rb +0 -3
  66. data/spec/selenium_spec_firefox_remote.rb +0 -3
  67. data/spec/selenium_spec_ie.rb +3 -1
  68. data/spec/server_spec.rb +1 -1
  69. data/spec/shared_selenium_session.rb +1 -1
  70. data/spec/spec_helper.rb +9 -2
  71. metadata +54 -2
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:field, locator_type: [String, Symbol]) do
4
+ visible { |options| :hidden if options[:type].to_s == 'hidden' }
5
+
6
+ xpath do |locator, **options|
7
+ invalid_types = %w[submit image]
8
+ invalid_types << 'hidden' unless options[:type].to_s == 'hidden'
9
+ xpath = XPath.descendant(:input, :textarea, :select)[!XPath.attr(:type).one_of(*invalid_types)]
10
+ locate_field(xpath, locator, options)
11
+ end
12
+
13
+ expression_filter(:type) do |expr, type|
14
+ type = type.to_s
15
+ if %w[textarea select].include?(type)
16
+ expr.self(type.to_sym)
17
+ else
18
+ expr[XPath.attr(:type) == type]
19
+ end
20
+ end
21
+
22
+ filter_set(:_field) # checked/unchecked/disabled/multiple/name/placeholder
23
+
24
+ node_filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }
25
+
26
+ node_filter(:with) do |node, with|
27
+ val = node.value
28
+ (with.is_a?(Regexp) ? with.match?(val) : val == with.to_s).tap do |res|
29
+ add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
30
+ end
31
+ end
32
+
33
+ describe_expression_filters do |type: nil, **|
34
+ " of type #{type.inspect}" if type
35
+ end
36
+
37
+ describe_node_filters do |**options|
38
+ " with value #{options[:with].to_s.inspect}" if options.key?(:with)
39
+ end
40
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:fieldset, locator_type: [String, Symbol]) do
4
+ xpath do |locator, legend: nil, **|
5
+ locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]
6
+ locator_matchers |= XPath.attr(test_id) == locator.to_s if test_id
7
+ xpath = XPath.descendant(:fieldset)[locator && locator_matchers]
8
+ xpath = xpath[XPath.child(:legend)[XPath.string.n.is(legend)]] if legend
9
+ xpath
10
+ end
11
+
12
+ node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
13
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
14
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:file_field, locator_type: [String, Symbol]) do
4
+ label 'file field'
5
+ xpath do |locator, allow_self: nil, **options|
6
+ xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
7
+ XPath.attr(:type) == 'file'
8
+ ]
9
+ locate_field(xpath, locator, options)
10
+ end
11
+
12
+ filter_set(:_field, %i[disabled multiple name])
13
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:fillable_field, locator_type: [String, Symbol]) do
4
+ label 'field'
5
+ xpath do |locator, allow_self: nil, **options|
6
+ xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input, :textarea)[
7
+ !XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')
8
+ ]
9
+ locate_field(xpath, locator, options)
10
+ end
11
+
12
+ expression_filter(:type) do |expr, type|
13
+ type = type.to_s
14
+ if type == 'textarea'
15
+ expr.self(type.to_sym)
16
+ else
17
+ expr[XPath.attr(:type) == type]
18
+ end
19
+ end
20
+
21
+ filter_set(:_field, %i[disabled multiple name placeholder])
22
+
23
+ node_filter(:with) do |node, with|
24
+ val = node.value
25
+ (with.is_a?(Regexp) ? with.match?(val) : val == with.to_s).tap do |res|
26
+ add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
27
+ end
28
+ end
29
+
30
+ describe_node_filters do |**options|
31
+ " with value #{options[:with].to_s.inspect}" if options.key?(:with)
32
+ end
33
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:frame, locator_type: [String, Symbol]) do
4
+ xpath do |locator, name: nil, **|
5
+ xpath = XPath.descendant(:iframe).union(XPath.descendant(:frame))
6
+ unless locator.nil?
7
+ locator_matchers = (XPath.attr(:id) == locator.to_s) | (XPath.attr(:name) == locator.to_s)
8
+ locator_matchers |= XPath.attr(test_id) == locator if test_id
9
+ xpath = xpath[locator_matchers]
10
+ end
11
+ xpath[find_by_attr(:name, name)]
12
+ end
13
+
14
+ describe_expression_filters do |name: nil, **|
15
+ " with name #{name}" if name
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:id, locator_type: [String, Symbol, Regexp]) do
4
+ xpath { |id| builder(XPath.descendant).add_attribute_conditions(id: id) }
5
+ locator_filter { |node, id| id.is_a?(Regexp) ? id.match?(node[:id]) : true }
6
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:label, locator_type: [String, Symbol]) do
4
+ label 'label'
5
+ xpath(:for) do |locator, options|
6
+ xpath = XPath.descendant(:label)
7
+ unless locator.nil?
8
+ locator_matchers = XPath.string.n.is(locator.to_s) | (XPath.attr(:id) == locator.to_s)
9
+ locator_matchers |= XPath.attr(test_id) == locator if test_id
10
+ xpath = xpath[locator_matchers]
11
+ end
12
+ if options.key?(:for)
13
+ if (for_option = options[:for].is_a?(Capybara::Node::Element) ? options[:for][:id] : options[:for])
14
+ with_attr = XPath.attr(:for) == for_option.to_s
15
+ labelable_elements = %i[button input keygen meter output progress select textarea]
16
+ wrapped = !XPath.attr(:for) &
17
+ XPath.descendant(*labelable_elements)[XPath.attr(:id) == for_option.to_s]
18
+ xpath = xpath[with_attr | wrapped]
19
+ end
20
+ end
21
+ xpath
22
+ end
23
+
24
+ node_filter(:for) do |node, field_or_value|
25
+ # Non element values were handled through the expression filter
26
+ next true unless field_or_value.is_a? Capybara::Node::Element
27
+
28
+ if (for_val = node[:for])
29
+ field_or_value[:id] == for_val
30
+ else
31
+ field_or_value.find_xpath('./ancestor::label[1]').include? node.base
32
+ end
33
+ end
34
+
35
+ describe_expression_filters do |**options|
36
+ next unless options.key?(:for) && !options[:for].is_a?(Capybara::Node::Element)
37
+
38
+ " for element with id of \"#{options[:for]}\""
39
+ end
40
+ describe_node_filters do |**options|
41
+ " for element #{options[:for]}" if options[:for]&.is_a?(Capybara::Node::Element)
42
+ end
43
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:link, locator_type: [String, Symbol]) do
4
+ xpath do |locator, href: true, alt: nil, title: nil, **|
5
+ xpath = builder(XPath.descendant(:a)).add_attribute_conditions(href: href)
6
+
7
+ unless locator.nil?
8
+ locator = locator.to_s
9
+ matchers = [XPath.attr(:id) == locator,
10
+ XPath.string.n.is(locator),
11
+ XPath.attr(:title).is(locator),
12
+ XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
13
+ matchers << XPath.attr(:'aria-label').is(locator) if enable_aria_label
14
+ matchers << XPath.attr(test_id).equals(locator) if test_id
15
+ xpath = xpath[matchers.reduce(:|)]
16
+ end
17
+
18
+ xpath = xpath[find_by_attr(:title, title)]
19
+ xpath = xpath[XPath.descendant(:img)[XPath.attr(:alt) == alt]] if alt
20
+ xpath
21
+ end
22
+
23
+ node_filter(:href) do |node, href|
24
+ # If not a Regexp it's been handled in the main XPath
25
+ (href.is_a?(Regexp) ? node[:href].match?(href) : true).tap do |res|
26
+ add_error "Expected href to match #{href.inspect} but it was #{node[:href].inspect}" unless res
27
+ end
28
+ end
29
+
30
+ expression_filter(:download, valid_values: [true, false, String]) do |expr, download|
31
+ builder(expr).add_attribute_conditions(download: download)
32
+ end
33
+
34
+ describe_expression_filters do |download: nil, **options|
35
+ desc = +''
36
+ if (href = options[:href])
37
+ desc << " with href #{'matching ' if href.is_a? Regexp}#{href.inspect}"
38
+ elsif options.key?(:href) # is nil/false specified?
39
+ desc << ' with no href attribute'
40
+ end
41
+ desc << " with download attribute#{" #{download}" if download.is_a? String}" if download
42
+ desc << ' without download attribute' if download == false
43
+ desc
44
+ end
45
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:link_or_button, locator_type: [String, Symbol]) do
4
+ label 'link or button'
5
+ xpath do |locator, **options|
6
+ %i[link button].map do |selector|
7
+ expression_for(selector, locator, **options)
8
+ end.reduce(:union)
9
+ end
10
+
11
+ node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
12
+
13
+ describe_node_filters do |disabled: nil, **|
14
+ ' that is disabled' if disabled == true
15
+ end
16
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:option, locator_type: [String, Symbol]) do
4
+ xpath do |locator|
5
+ xpath = XPath.descendant(:option)
6
+ xpath = xpath[XPath.string.n.is(locator.to_s)] unless locator.nil?
7
+ xpath
8
+ end
9
+
10
+ node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
11
+ expression_filter(:disabled) { |xpath, val| val ? xpath : xpath[~XPath.attr(:disabled)] }
12
+
13
+ node_filter(:selected, :boolean) { |node, value| !(value ^ node.selected?) }
14
+
15
+ describe_expression_filters do |disabled: nil, **options|
16
+ desc = +''
17
+ desc << ' that is not disabled' if disabled == false
18
+ (expression_filters.keys & options.keys).inject(desc) { |memo, ef| memo << " with #{ef} #{options[ef]}" }
19
+ end
20
+
21
+ describe_node_filters do |**options|
22
+ desc = +''
23
+ desc << ' that is disabled' if options[:disabled]
24
+ desc << " that is#{' not' unless options[:selected]} selected" if options.key?(:selected)
25
+ desc
26
+ end
27
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:radio_button, locator_type: [String, Symbol]) do
4
+ label 'radio button'
5
+ xpath do |locator, allow_self: nil, **options|
6
+ xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
7
+ XPath.attr(:type) == 'radio'
8
+ ]
9
+ locate_field(xpath, locator, options)
10
+ end
11
+
12
+ filter_set(:_field, %i[checked unchecked disabled name])
13
+
14
+ node_filter(:option) do |node, value|
15
+ val = node.value
16
+ (val == value.to_s).tap do |res|
17
+ add_error("Expected option value to be #{value.inspect} but it was #{val.inspect}") unless res
18
+ end
19
+ end
20
+
21
+ describe_node_filters do |option: nil, **|
22
+ " with value #{option.inspect}" if option
23
+ end
24
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:select, locator_type: [String, Symbol]) do
4
+ label 'select box'
5
+
6
+ xpath do |locator, **options|
7
+ xpath = XPath.descendant(:select)
8
+ locate_field(xpath, locator, options)
9
+ end
10
+
11
+ filter_set(:_field, %i[disabled multiple name placeholder])
12
+
13
+ node_filter(:options) do |node, options|
14
+ actual = if node.visible?
15
+ node.all(:xpath, './/option', wait: false).map(&:text)
16
+ else
17
+ node.all(:xpath, './/option', visible: false, wait: false).map { |option| option.text(:all) }
18
+ end
19
+ (options.sort == actual.sort).tap do |res|
20
+ add_error("Expected options #{options.inspect} found #{actual.inspect}") unless res
21
+ end
22
+ end
23
+
24
+ expression_filter(:with_options) do |expr, options|
25
+ options.inject(expr) do |xpath, option|
26
+ xpath[expression_for(:option, option)]
27
+ end
28
+ end
29
+
30
+ node_filter(:selected) do |node, selected|
31
+ actual = node.all(:xpath, './/option', visible: false, wait: false)
32
+ .select(&:selected?)
33
+ .map { |option| option.text(:all) }
34
+ (Array(selected).sort == actual.sort).tap do |res|
35
+ add_error("Expected #{selected.inspect} to be selected found #{actual.inspect}") unless res
36
+ end
37
+ end
38
+
39
+ node_filter(:with_selected) do |node, selected|
40
+ actual = node.all(:xpath, './/option', visible: false, wait: false)
41
+ .select(&:selected?)
42
+ .map { |option| option.text(:all) }
43
+ (Array(selected) - actual).empty?.tap do |res|
44
+ add_error("Expected at least #{selected.inspect} to be selected found #{actual.inspect}") unless res
45
+ end
46
+ end
47
+
48
+ describe_expression_filters do |with_options: nil, **|
49
+ desc = +''
50
+ desc << " with at least options #{with_options.inspect}" if with_options
51
+ desc
52
+ end
53
+
54
+ describe_node_filters do |options: nil, selected: nil, with_selected: nil, disabled: nil, **|
55
+ desc = +''
56
+ desc << " with options #{options.inspect}" if options
57
+ desc << " with #{selected.inspect} selected" if selected
58
+ desc << " with at least #{with_selected.inspect} selected" if with_selected
59
+ desc << ' which is disabled' if disabled
60
+ desc
61
+ end
62
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ Capybara.add_selector(:table, locator_type: [String, Symbol]) do
4
+ xpath do |locator, caption: nil, **|
5
+ xpath = XPath.descendant(:table)
6
+ unless locator.nil?
7
+ locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.descendant(:caption).is(locator.to_s)
8
+ locator_matchers |= XPath.attr(test_id) == locator if test_id
9
+ xpath = xpath[locator_matchers]
10
+ end
11
+ xpath = xpath[XPath.descendant(:caption) == caption] if caption
12
+ xpath
13
+ end
14
+
15
+ expression_filter(:with_cols, valid_values: [Array]) do |xpath, cols|
16
+ col_conditions = cols.map do |col|
17
+ if col.is_a? Hash
18
+ col.reduce(nil) do |xp, (header, cell_str)|
19
+ header = XPath.descendant(:th)[XPath.string.n.is(header)]
20
+ td = XPath.descendant(:tr)[header].descendant(:td)
21
+ cell_condition = XPath.string.n.is(cell_str)
22
+ cell_condition &= prev_col_position?(XPath.ancestor(:table)[1].join(xp)) if xp
23
+ td[cell_condition]
24
+ end
25
+ else
26
+ cells_xp = col.reduce(nil) do |prev_cell, cell_str|
27
+ cell_condition = XPath.string.n.is(cell_str)
28
+
29
+ if prev_cell
30
+ prev_cell = XPath.ancestor(:tr)[1].preceding_sibling(:tr).join(prev_cell)
31
+ cell_condition &= prev_col_position?(prev_cell)
32
+ end
33
+
34
+ XPath.descendant(:td)[cell_condition]
35
+ end
36
+ XPath.descendant(:tr).join(cells_xp)
37
+ end
38
+ end.reduce(:&)
39
+ xpath[col_conditions]
40
+ end
41
+
42
+ expression_filter(:cols, valid_values: [Array]) do |xpath, cols|
43
+ raise ArgumentError, ':cols must be an Array of Arrays' unless cols.all? { |col| col.is_a? Array }
44
+
45
+ rows = cols.transpose
46
+ col_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
47
+ xpath[match_row_count(rows.size)][col_conditions]
48
+ end
49
+
50
+ expression_filter(:with_rows, valid_values: [Array]) do |xpath, rows|
51
+ rows_conditions = rows.map { |row| match_row(row) }.reduce(:&)
52
+ xpath[rows_conditions]
53
+ end
54
+
55
+ expression_filter(:rows, valid_values: [Array]) do |xpath, rows|
56
+ rows_conditions = rows.map { |row| match_row(row, match_size: true) }.reduce(:&)
57
+ xpath[match_row_count(rows.size)][rows_conditions]
58
+ end
59
+
60
+ describe_expression_filters do |caption: nil, **|
61
+ " with caption \"#{caption}\"" if caption
62
+ end
63
+
64
+ def prev_col_position?(cell)
65
+ XPath.position.equals(cell_position(cell))
66
+ end
67
+
68
+ def cell_position(cell)
69
+ cell.preceding_sibling(:td).count.plus(1)
70
+ end
71
+
72
+ def match_row(row, match_size: false)
73
+ xp = XPath.descendant(:tr)[
74
+ if row.is_a? Hash
75
+ row_match_cells_to_headers(row)
76
+ else
77
+ XPath.descendant(:td)[row_match_ordered_cells(row)]
78
+ end
79
+ ]
80
+ xp = xp[XPath.descendant(:td).count.equals(row.size)] if match_size
81
+ xp
82
+ end
83
+
84
+ def match_row_count(size)
85
+ XPath.descendant(:tbody).descendant(:tr).count.equals(size) |
86
+ (XPath.descendant(:tr).count.equals(size) & ~XPath.descendant(:tbody))
87
+ end
88
+
89
+ def row_match_cells_to_headers(row)
90
+ row.map do |header, cell|
91
+ header_xp = XPath.ancestor(:table)[1].descendant(:tr)[1].descendant(:th)[XPath.string.n.is(header)]
92
+ XPath.descendant(:td)[
93
+ XPath.string.n.is(cell) & XPath.position.equals(header_xp.preceding_sibling.count.plus(1))
94
+ ]
95
+ end.reduce(:&)
96
+ end
97
+
98
+ def row_match_ordered_cells(row)
99
+ row_conditions = row.map do |cell|
100
+ XPath.self(:td)[XPath.string.n.is(cell)]
101
+ end
102
+ row_conditions.reverse.reduce do |cond, cell|
103
+ cell[XPath.following_sibling[cond]]
104
+ end
105
+ end
106
+ end