capybara 3.18.0 → 3.19.0

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