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.
- checksums.yaml +4 -4
- data/History.md +16 -0
- data/README.md +14 -44
- data/lib/capybara/node/actions.rb +2 -2
- data/lib/capybara/node/element.rb +3 -5
- data/lib/capybara/queries/selector_query.rb +30 -11
- data/lib/capybara/rack_test/node.rb +1 -1
- data/lib/capybara/result.rb +2 -0
- data/lib/capybara/rspec/matcher_proxies.rb +2 -0
- data/lib/capybara/rspec/matchers/base.rb +2 -2
- data/lib/capybara/rspec/matchers/count_sugar.rb +36 -0
- data/lib/capybara/rspec/matchers/have_selector.rb +3 -0
- data/lib/capybara/rspec/matchers/have_text.rb +3 -0
- data/lib/capybara/selector.rb +196 -599
- data/lib/capybara/selector/css.rb +2 -0
- data/lib/capybara/selector/definition.rb +276 -0
- data/lib/capybara/selector/definition/button.rb +46 -0
- data/lib/capybara/selector/definition/checkbox.rb +23 -0
- data/lib/capybara/selector/definition/css.rb +5 -0
- data/lib/capybara/selector/definition/datalist_input.rb +35 -0
- data/lib/capybara/selector/definition/datalist_option.rb +25 -0
- data/lib/capybara/selector/definition/element.rb +27 -0
- data/lib/capybara/selector/definition/field.rb +40 -0
- data/lib/capybara/selector/definition/fieldset.rb +14 -0
- data/lib/capybara/selector/definition/file_field.rb +13 -0
- data/lib/capybara/selector/definition/fillable_field.rb +33 -0
- data/lib/capybara/selector/definition/frame.rb +17 -0
- data/lib/capybara/selector/definition/id.rb +6 -0
- data/lib/capybara/selector/definition/label.rb +43 -0
- data/lib/capybara/selector/definition/link.rb +45 -0
- data/lib/capybara/selector/definition/link_or_button.rb +16 -0
- data/lib/capybara/selector/definition/option.rb +27 -0
- data/lib/capybara/selector/definition/radio_button.rb +24 -0
- data/lib/capybara/selector/definition/select.rb +62 -0
- data/lib/capybara/selector/definition/table.rb +106 -0
- data/lib/capybara/selector/definition/table_row.rb +21 -0
- data/lib/capybara/selector/definition/xpath.rb +5 -0
- data/lib/capybara/selector/filters/base.rb +4 -0
- data/lib/capybara/selector/filters/locator_filter.rb +12 -2
- data/lib/capybara/selector/selector.rb +40 -452
- data/lib/capybara/selenium/driver.rb +4 -10
- data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +3 -9
- data/lib/capybara/selenium/driver_specializations/internet_explorer_driver.rb +8 -0
- data/lib/capybara/selenium/extensions/find.rb +1 -1
- data/lib/capybara/selenium/logger_suppressor.rb +5 -0
- data/lib/capybara/selenium/node.rb +19 -13
- data/lib/capybara/selenium/nodes/chrome_node.rb +30 -0
- data/lib/capybara/selenium/nodes/firefox_node.rb +14 -12
- data/lib/capybara/selenium/nodes/ie_node.rb +11 -0
- data/lib/capybara/selenium/nodes/safari_node.rb +7 -12
- data/lib/capybara/server/checker.rb +7 -3
- data/lib/capybara/session.rb +2 -2
- data/lib/capybara/spec/session/all_spec.rb +1 -1
- data/lib/capybara/spec/session/find_spec.rb +1 -1
- data/lib/capybara/spec/session/first_spec.rb +1 -1
- data/lib/capybara/spec/session/has_css_spec.rb +7 -0
- data/lib/capybara/spec/session/has_text_spec.rb +6 -0
- data/lib/capybara/spec/session/save_screenshot_spec.rb +11 -0
- data/lib/capybara/spec/session/select_spec.rb +0 -5
- data/lib/capybara/spec/test_app.rb +8 -3
- data/lib/capybara/version.rb +1 -1
- data/lib/capybara/window.rb +1 -1
- data/spec/minitest_spec_spec.rb +1 -0
- data/spec/selector_spec.rb +12 -6
- data/spec/selenium_spec_firefox.rb +0 -3
- data/spec/selenium_spec_firefox_remote.rb +0 -3
- data/spec/selenium_spec_ie.rb +3 -1
- data/spec/server_spec.rb +1 -1
- data/spec/shared_selenium_session.rb +1 -1
- data/spec/spec_helper.rb +9 -2
- 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,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
|