capybara 3.1.1 → 3.2.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 (73) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +19 -0
  3. data/README.md +1 -1
  4. data/lib/capybara.rb +2 -0
  5. data/lib/capybara/config.rb +2 -1
  6. data/lib/capybara/driver/base.rb +1 -1
  7. data/lib/capybara/driver/node.rb +3 -3
  8. data/lib/capybara/node/actions.rb +90 -92
  9. data/lib/capybara/node/base.rb +2 -2
  10. data/lib/capybara/node/document_matchers.rb +5 -5
  11. data/lib/capybara/node/element.rb +47 -16
  12. data/lib/capybara/node/finders.rb +13 -13
  13. data/lib/capybara/node/matchers.rb +18 -17
  14. data/lib/capybara/node/simple.rb +6 -2
  15. data/lib/capybara/queries/ancestor_query.rb +1 -1
  16. data/lib/capybara/queries/base_query.rb +3 -3
  17. data/lib/capybara/queries/current_path_query.rb +1 -1
  18. data/lib/capybara/queries/match_query.rb +8 -0
  19. data/lib/capybara/queries/selector_query.rb +97 -42
  20. data/lib/capybara/queries/sibling_query.rb +1 -1
  21. data/lib/capybara/queries/text_query.rb +12 -7
  22. data/lib/capybara/rack_test/browser.rb +9 -7
  23. data/lib/capybara/rack_test/form.rb +15 -17
  24. data/lib/capybara/rack_test/node.rb +12 -12
  25. data/lib/capybara/result.rb +26 -15
  26. data/lib/capybara/rspec.rb +1 -2
  27. data/lib/capybara/rspec/compound.rb +4 -4
  28. data/lib/capybara/rspec/matchers.rb +2 -2
  29. data/lib/capybara/selector.rb +75 -225
  30. data/lib/capybara/selector/css.rb +2 -2
  31. data/lib/capybara/selector/filter_set.rb +17 -21
  32. data/lib/capybara/selector/filters/base.rb +24 -1
  33. data/lib/capybara/selector/filters/expression_filter.rb +3 -5
  34. data/lib/capybara/selector/filters/node_filter.rb +4 -4
  35. data/lib/capybara/selector/selector.rb +221 -69
  36. data/lib/capybara/selenium/driver.rb +15 -88
  37. data/lib/capybara/selenium/node.rb +25 -28
  38. data/lib/capybara/server.rb +10 -54
  39. data/lib/capybara/server/animation_disabler.rb +43 -0
  40. data/lib/capybara/server/middleware.rb +55 -0
  41. data/lib/capybara/session.rb +29 -30
  42. data/lib/capybara/session/config.rb +11 -1
  43. data/lib/capybara/session/matchers.rb +5 -5
  44. data/lib/capybara/spec/session/assert_text_spec.rb +1 -1
  45. data/lib/capybara/spec/session/body_spec.rb +10 -12
  46. data/lib/capybara/spec/session/click_link_spec.rb +3 -3
  47. data/lib/capybara/spec/session/element/assert_match_selector_spec.rb +1 -1
  48. data/lib/capybara/spec/session/fill_in_spec.rb +9 -0
  49. data/lib/capybara/spec/session/find_field_spec.rb +1 -1
  50. data/lib/capybara/spec/session/find_spec.rb +8 -3
  51. data/lib/capybara/spec/session/has_link_spec.rb +2 -2
  52. data/lib/capybara/spec/session/node_spec.rb +50 -0
  53. data/lib/capybara/spec/session/node_wrapper_spec.rb +5 -5
  54. data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +1 -1
  55. data/lib/capybara/spec/session/window/windows_spec.rb +3 -5
  56. data/lib/capybara/spec/spec_helper.rb +4 -2
  57. data/lib/capybara/spec/views/with_animation.erb +46 -0
  58. data/lib/capybara/version.rb +1 -1
  59. data/lib/capybara/window.rb +3 -2
  60. data/spec/filter_set_spec.rb +19 -2
  61. data/spec/result_spec.rb +33 -1
  62. data/spec/rspec/features_spec.rb +6 -10
  63. data/spec/rspec/shared_spec_matchers.rb +4 -4
  64. data/spec/selector_spec.rb +74 -4
  65. data/spec/selenium_spec_marionette.rb +2 -0
  66. data/spec/server_spec.rb +1 -1
  67. data/spec/session_spec.rb +12 -0
  68. data/spec/shared_selenium_session.rb +30 -0
  69. metadata +8 -9
  70. data/.yard/templates_custom/default/class/html/selectors.erb +0 -38
  71. data/.yard/templates_custom/default/class/html/setup.rb +0 -17
  72. data/.yard/yard_extensions.rb +0 -78
  73. data/.yardopts +0 -1
@@ -16,7 +16,7 @@ module Capybara
16
16
 
17
17
  def description
18
18
  desc = super
19
- sibling_query = @sibling_node && @sibling_node.instance_variable_get(:@query)
19
+ sibling_query = @sibling_node&.instance_variable_get(:@query)
20
20
  desc += " that is a sibling of #{sibling_query.description}" if sibling_query
21
21
  desc
22
22
  end
@@ -50,15 +50,15 @@ module Capybara
50
50
  end
51
51
 
52
52
  def build_message(report_on_invisible)
53
- message = "".dup
53
+ message = +""
54
54
  unless (COUNT_KEYS & @options.keys).empty?
55
55
  message << " but found #{@count} #{Capybara::Helpers.declension('time', 'times', @count)}"
56
56
  end
57
57
  message << " in #{@actual_text.inspect}"
58
58
 
59
59
  details_message = []
60
- details_message << case_insensitive_message if @node and !@expected_text.is_a? Regexp
61
- details_message << invisible_message if @node and check_visible_text? and report_on_invisible
60
+ details_message << case_insensitive_message if @node && check_case_insensitive?
61
+ details_message << invisible_message if @node && check_visible_text? && report_on_invisible
62
62
  details_message.compact!
63
63
 
64
64
  message << ". (However, #{details_message.join(' and ')}.)" unless details_message.empty?
@@ -75,10 +75,11 @@ module Capybara
75
75
  def invisible_message
76
76
  invisible_text = text(@node, :all)
77
77
  invisible_count = invisible_text.scan(@search_regexp).size
78
- if invisible_count != @count
79
- "it was found #{invisible_count} #{Capybara::Helpers.declension('time', 'times', invisible_count)} including non-visible text"
80
- end
81
- rescue # An error getting the non-visible text (if element goes out of scope) should not affect the response
78
+ return if invisible_count == @count
79
+ "it was found #{invisible_count} #{Capybara::Helpers.declension('time', 'times', invisible_count)} including non-visible text"
80
+ rescue StandardError
81
+ # An error getting the non-visible text (if element goes out of scope) should not affect the response
82
+ nil
82
83
  end
83
84
 
84
85
  def valid_keys
@@ -89,6 +90,10 @@ module Capybara
89
90
  @type == :visible
90
91
  end
91
92
 
93
+ def check_case_insensitive?
94
+ !@expected_text.is_a?(Regexp)
95
+ end
96
+
92
97
  def text(node, query_type)
93
98
  node.text(query_type)
94
99
  end
@@ -34,7 +34,7 @@ class Capybara::RackTest::Browser
34
34
  end
35
35
 
36
36
  def follow(method, path, **attributes)
37
- return if path.gsub(/^#{Regexp.escape(request_path)}/, '').start_with?('#') || path.downcase.start_with?('javascript:')
37
+ return if fragment_or_script?(path)
38
38
  process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => current_url)
39
39
  end
40
40
 
@@ -63,9 +63,7 @@ class Capybara::RackTest::Browser
63
63
  new_uri.host ||= @current_host
64
64
  new_uri.port ||= @current_port unless new_uri.default_port == @current_port
65
65
 
66
- @current_scheme = new_uri.scheme
67
- @current_host = new_uri.host
68
- @current_port = new_uri.port
66
+ @current_scheme, @current_host, @current_port = new_uri.select(:scheme, :host, :port)
69
67
 
70
68
  reset_cache!
71
69
  send(method, new_uri.to_s, attributes, env.merge(options[:headers] || {}))
@@ -79,9 +77,7 @@ class Capybara::RackTest::Browser
79
77
 
80
78
  def reset_host!
81
79
  uri = URI.parse(driver.session_options.app_host || driver.session_options.default_host)
82
- @current_scheme = uri.scheme
83
- @current_host = uri.host
84
- @current_port = uri.port
80
+ @current_scheme, @current_host, @current_port = uri.select(:scheme, :host, :port)
85
81
  end
86
82
 
87
83
  def reset_cache!
@@ -122,4 +118,10 @@ protected
122
118
  rescue Rack::Test::Error
123
119
  "/"
124
120
  end
121
+
122
+ private
123
+
124
+ def fragment_or_script?(path)
125
+ path.gsub(/^#{Regexp.escape(request_path)}/, '').start_with?('#') || path.downcase.start_with?('javascript:')
126
+ end
125
127
  end
@@ -30,12 +30,9 @@ class Capybara::RackTest::Form < Capybara::RackTest::Node
30
30
 
31
31
  native.xpath(form_elements_xpath).map do |field|
32
32
  case field.name
33
- when 'input'
34
- add_input_param(field, params)
35
- when 'select'
36
- add_select_param(field, params)
37
- when 'textarea'
38
- add_textarea_param(field, params)
33
+ when 'input' then add_input_param(field, params)
34
+ when 'select' then add_select_param(field, params)
35
+ when 'textarea' then add_textarea_param(field, params)
39
36
  end
40
37
  end
41
38
  merge_param!(params, button[:name], button[:value] || "") if button[:name]
@@ -44,8 +41,8 @@ class Capybara::RackTest::Form < Capybara::RackTest::Node
44
41
  end
45
42
 
46
43
  def submit(button)
47
- action = (button && button['formaction']) || native['action']
48
- method = (button && button['formmethod']) || request_method
44
+ action = button&.[]('formaction') || native['action']
45
+ method = button&.[]('formmethod') || request_method
49
46
  driver.submit(method, action.to_s, params(button))
50
47
  end
51
48
 
@@ -66,6 +63,7 @@ private
66
63
  end
67
64
 
68
65
  def merge_param!(params, key, value)
66
+ key = key.to_s
69
67
  if Rack::Utils.respond_to?(:default_query_parser)
70
68
  Rack::Utils.default_query_parser.normalize_params(params, key, value, Rack::Utils.param_depth_limit)
71
69
  else
@@ -85,7 +83,7 @@ private
85
83
  if %w[radio checkbox].include? field['type']
86
84
  if field['checked']
87
85
  node = Capybara::RackTest::Node.new(driver, field)
88
- merge_param!(params, field['name'].to_s, node.value.to_s)
86
+ merge_param!(params, field['name'], node.value.to_s)
89
87
  end
90
88
  elsif %w[submit image].include? field['type']
91
89
  # TODO: identify the click button here (in document order, rather
@@ -96,29 +94,29 @@ private
96
94
  NilUploadedFile.new
97
95
  else
98
96
  mime_info = MiniMime.lookup_by_filename(value)
99
- Rack::Test::UploadedFile.new(value, (mime_info && mime_info.content_type).to_s)
97
+ Rack::Test::UploadedFile.new(value, mime_info&.content_type&.to_s)
100
98
  end
101
- merge_param!(params, field['name'].to_s, file)
99
+ merge_param!(params, field['name'], file)
102
100
  else
103
- merge_param!(params, field['name'].to_s, File.basename(field['value'].to_s))
101
+ merge_param!(params, field['name'], File.basename(field['value'].to_s))
104
102
  end
105
103
  else
106
- merge_param!(params, field['name'].to_s, field['value'].to_s)
104
+ merge_param!(params, field['name'], field['value'].to_s)
107
105
  end
108
106
  end
109
107
 
110
108
  def add_select_param(field, params)
111
- if field['multiple'] == 'multiple'
109
+ if field.has_attribute?('multiple')
112
110
  field.xpath(".//option[@selected]").each do |option|
113
- merge_param!(params, field['name'].to_s, (option['value'] || option.text).to_s)
111
+ merge_param!(params, field['name'], (option['value'] || option.text).to_s)
114
112
  end
115
113
  else
116
114
  option = field.xpath('.//option[@selected]').first || field.xpath('.//option').first
117
- merge_param!(params, field['name'].to_s, (option['value'] || option.text).to_s) if option
115
+ merge_param!(params, field['name'], (option['value'] || option.text).to_s) if option
118
116
  end
119
117
  end
120
118
 
121
119
  def add_textarea_param(field, params)
122
- merge_param!(params, field['name'].to_s, field['_capybara_raw_value'].to_s.gsub(/\n/, "\r\n"))
120
+ merge_param!(params, field['name'], field['_capybara_raw_value'].to_s.gsub(/\n/, "\r\n"))
123
121
  end
124
122
  end
@@ -55,7 +55,7 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
55
55
  native.remove_attribute('selected')
56
56
  end
57
57
 
58
- def click(keys = [], offset = {})
58
+ def click(keys = [], **offset)
59
59
  raise ArgumentError, "The RackTest driver does not support click options" unless keys.empty? && offset.empty?
60
60
 
61
61
  if link?
@@ -218,36 +218,36 @@ private
218
218
  end
219
219
 
220
220
  def submits?
221
- (tag_name == 'input' and %w[submit image].include?(type)) || (tag_name == 'button' and [nil, "submit"].include?(type))
221
+ (tag_name == 'input' && %w[submit image].include?(type)) || (tag_name == 'button' && [nil, "submit"].include?(type))
222
222
  end
223
223
 
224
224
  def checkable?
225
- tag_name == 'input' and %w[checkbox radio].include?(type)
225
+ tag_name == 'input' && %w[checkbox radio].include?(type)
226
226
  end
227
227
 
228
228
  protected
229
229
 
230
230
  def checkbox_or_radio?(field = self)
231
- field && (field.checkbox? || field.radio?)
231
+ field&.checkbox? || field&.radio?
232
232
  end
233
233
 
234
234
  def checkbox?
235
235
  input_field? && type == 'checkbox'
236
236
  end
237
237
 
238
- def input_field?
239
- tag_name == 'input'
240
- end
241
-
242
238
  def radio?
243
239
  input_field? && type == 'radio'
244
240
  end
245
241
 
246
- def textarea?
247
- tag_name == "textarea"
248
- end
249
-
250
242
  def text_or_password?
251
243
  input_field? && (type == 'text' || type == 'password')
252
244
  end
245
+
246
+ def input_field?
247
+ tag_name == 'input'
248
+ end
249
+
250
+ def textarea?
251
+ tag_name == "textarea"
252
+ end
253
253
  end
@@ -47,14 +47,27 @@ module Capybara
47
47
  end
48
48
 
49
49
  def [](*args)
50
- if (args.size == 1) && ((idx = args[0]).is_a? Integer) && (idx >= 0)
51
- @result_cache << @results_enum.next while @result_cache.size <= idx
52
- @result_cache[idx]
53
- else
50
+ idx, length = args
51
+ max_idx = case idx
52
+ when Integer
53
+ if !idx.negative?
54
+ length.nil? ? idx : idx + length - 1
55
+ else
56
+ nil
57
+ end
58
+ when Range
59
+ idx.max
60
+ end
61
+
62
+ if max_idx.nil?
54
63
  full_results[*args]
64
+ else
65
+ loop do
66
+ break if @result_cache.size > max_idx
67
+ @result_cache << @results_enum.next
68
+ end
69
+ @result_cache[*args]
55
70
  end
56
- rescue StopIteration
57
- return nil
58
71
  end
59
72
  alias :at :[]
60
73
 
@@ -84,10 +97,9 @@ module Capybara
84
97
 
85
98
  if @query.options[:maximum]
86
99
  max_opt = Integer(@query.options[:maximum])
87
- begin
88
- @result_cache << @results_enum.next while @result_cache.size <= max_opt
89
- return 1
90
- rescue StopIteration
100
+ loop do
101
+ return 1 if @result_cache.size > max_opt
102
+ @result_cache << @results_enum.next
91
103
  end
92
104
  end
93
105
 
@@ -97,12 +109,11 @@ module Capybara
97
109
  break if @result_cache.size > max
98
110
  @result_cache << @results_enum.next
99
111
  end
100
- return 0 if @query.options[:between].include?(@result_cache.size)
101
- return -1 if @result_cache.size < min
102
- return 1
112
+ return 0 if @query.options[:between].include? @result_cache.size
113
+ return @result_cache.size <=> min
103
114
  end
104
115
 
105
- return 0
116
+ 0
106
117
  end
107
118
 
108
119
  def matches_count?
@@ -117,7 +128,7 @@ module Capybara
117
128
  message << ", found #{count} #{Capybara::Helpers.declension('match', 'matches', count)}: " << full_results.map(&:text).map(&:inspect).join(", ")
118
129
  end
119
130
  unless rest.empty?
120
- elements = rest.map { |el| el.text rescue "<<ERROR>>" }.map(&:inspect).join(", ")
131
+ elements = rest.map { |el| el.text rescue "<<ERROR>>" }.map(&:inspect).join(", ") # rubocop:disable Style/RescueModifier
121
132
  message << ". Also found " << elements << ", which matched the selector but not all filters."
122
133
  end
123
134
  message
@@ -20,9 +20,8 @@ RSpec.configure do |config|
20
20
  end
21
21
  end
22
22
 
23
- config.before do
23
+ config.before do |example|
24
24
  if self.class.include?(Capybara::DSL)
25
- example = RSpec.current_example
26
25
  Capybara.current_driver = Capybara.javascript_driver if example.metadata[:js]
27
26
  Capybara.current_driver = example.metadata[:driver] if example.metadata[:driver]
28
27
  end
@@ -6,7 +6,7 @@ module Capybara
6
6
  include ::RSpec::Matchers::Composable
7
7
 
8
8
  def and(matcher)
9
- Capybara::RSpecMatchers::Compound::And.new(self, matcher)
9
+ And.new(self, matcher)
10
10
  end
11
11
 
12
12
  def and_then(matcher)
@@ -14,7 +14,7 @@ module Capybara
14
14
  end
15
15
 
16
16
  def or(matcher)
17
- Capybara::RSpecMatchers::Compound::Or.new(self, matcher)
17
+ Or.new(self, matcher)
18
18
  end
19
19
 
20
20
  class CapybaraEvaluator
@@ -44,7 +44,7 @@ module Capybara
44
44
  raise ::Capybara::ElementNotFound unless [matcher_1_matches?, matcher_2_matches?].all?
45
45
  true
46
46
  end
47
- rescue
47
+ rescue StandardError
48
48
  false
49
49
  end
50
50
  end
@@ -72,7 +72,7 @@ module Capybara
72
72
  raise ::Capybara::ElementNotFound unless [matcher_1_matches?, matcher_2_matches?].any?
73
73
  true
74
74
  end
75
- rescue
75
+ rescue StandardError
76
76
  false
77
77
  end
78
78
  end
@@ -25,14 +25,14 @@ module Capybara
25
25
  yield(wrap(actual))
26
26
  rescue Capybara::ExpectationNotMet => e
27
27
  @failure_message = e.message
28
- return false
28
+ false
29
29
  end
30
30
 
31
31
  def wrap_does_not_match?(actual)
32
32
  yield(wrap(actual))
33
33
  rescue Capybara::ExpectationNotMet => e
34
34
  @failure_message_when_negated = e.message
35
- return false
35
+ false
36
36
  end
37
37
 
38
38
  def session_query_args
@@ -2,16 +2,16 @@
2
2
 
3
3
  require 'capybara/selector/selector'
4
4
  Capybara::Selector::FilterSet.add(:_field) do
5
- filter(:checked, :boolean) { |node, value| !(value ^ node.checked?) }
6
- filter(:unchecked, :boolean) { |node, value| (value ^ node.checked?) }
7
- filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
8
- filter(:multiple, :boolean) { |node, value| !(value ^ node.multiple?) }
5
+ node_filter(:checked, :boolean) { |node, value| !(value ^ node.checked?) }
6
+ node_filter(:unchecked, :boolean) { |node, value| (value ^ node.checked?) }
7
+ node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
8
+ node_filter(:multiple, :boolean) { |node, value| !(value ^ node.multiple?) }
9
9
 
10
10
  expression_filter(:name) { |xpath, val| xpath[XPath.attr(:name) == val] }
11
11
  expression_filter(:placeholder) { |xpath, val| xpath[XPath.attr(:placeholder) == val] }
12
12
 
13
13
  describe do |checked: nil, unchecked: nil, disabled: nil, multiple: nil, **_options|
14
- desc, states = "".dup, []
14
+ desc, states = +"", []
15
15
  states << 'checked' if checked || (unchecked == false)
16
16
  states << 'not checked' if unchecked || (checked == false)
17
17
  states << 'disabled' if disabled == true
@@ -24,54 +24,19 @@ Capybara::Selector::FilterSet.add(:_field) do
24
24
  end
25
25
 
26
26
  # rubocop:disable Metrics/BlockLength
27
- # rubocop:disable Metrics/ParameterLists
28
-
29
- ##
30
- #
31
- # Select elements by XPath expression
32
- #
33
- # @locator An XPath expression
34
- #
27
+
35
28
  Capybara.add_selector(:xpath) do
36
29
  xpath { |xpath| xpath }
37
30
  end
38
31
 
39
- ##
40
- #
41
- # Select elements by CSS selector
42
- #
43
- # @locator A CSS selector
44
- #
45
32
  Capybara.add_selector(:css) do
46
33
  css { |css| css }
47
34
  end
48
35
 
49
- ##
50
- #
51
- # Select element by id
52
- #
53
- # @locator The id of the element to match
54
- #
55
36
  Capybara.add_selector(:id) do
56
37
  xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] }
57
38
  end
58
39
 
59
- ##
60
- #
61
- # Select field elements (input [not of type submit, image, or hidden], textarea, select)
62
- #
63
- # @locator Matches against the id, name, or placeholder
64
- # @filter [String] :id Matches the id attribute
65
- # @filter [String] :name Matches the name attribute
66
- # @filter [String] :placeholder Matches the placeholder attribute
67
- # @filter [String] :type Matches the type attribute of the field or element type for 'textarea' and 'select'
68
- # @filter [Boolean] :readonly
69
- # @filter [String] :with Matches the current value of the field
70
- # @filter [String, Array<String>] :class Matches the class(es) provided
71
- # @filter [Boolean] :checked Match checked fields?
72
- # @filter [Boolean] :unchecked Match unchecked fields?
73
- # @filter [Boolean] :disabled Match disabled field?
74
- # @filter [Boolean] :multiple Match fields that accept multiple values
75
40
  Capybara.add_selector(:field) do
76
41
  xpath do |locator, **options|
77
42
  xpath = XPath.descendant(:input, :textarea, :select)[!XPath.attr(:type).one_of('submit', 'image', 'hidden')]
@@ -89,12 +54,12 @@ Capybara.add_selector(:field) do
89
54
 
90
55
  filter_set(:_field) # checked/unchecked/disabled/multiple/name/placeholder
91
56
 
92
- filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }
93
- filter(:with) do |node, with|
57
+ node_filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }
58
+ node_filter(:with) do |node, with|
94
59
  with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
95
60
  end
96
61
  describe do |type: nil, **options|
97
- desc = "".dup
62
+ desc = +""
98
63
  (expression_filters.keys - [:type]).each { |ef| desc << " with #{ef} #{options[ef]}" if options.key?(ef) }
99
64
  desc << " of type #{type.inspect}" if type
100
65
  desc << " with value #{options[:with].to_s.inspect}" if options.key?(:with)
@@ -102,16 +67,6 @@ Capybara.add_selector(:field) do
102
67
  end
103
68
  end
104
69
 
105
- ##
106
- #
107
- # Select fieldset elements
108
- #
109
- # @locator Matches id or contents of wrapped legend
110
- #
111
- # @filter [String] :id Matches id attribute
112
- # @filter [String] :legend Matches contents of wrapped legend
113
- # @filter [String, Array<String>] :class Matches the class(es) provided
114
- #
115
70
  Capybara.add_selector(:fieldset) do
116
71
  xpath(:legend) do |locator, legend: nil, **_options|
117
72
  xpath = XPath.descendant(:fieldset)
@@ -121,18 +76,6 @@ Capybara.add_selector(:fieldset) do
121
76
  end
122
77
  end
123
78
 
124
- ##
125
- #
126
- # Find links ( <a> elements with an href attribute )
127
- #
128
- # @locator Matches the id or title attributes, or the string content of the link, or the alt attribute of a contained img element
129
- #
130
- # @filter [String] :id Matches the id attribute
131
- # @filter [String] :title Matches the title attribute
132
- # @filter [String] :alt Matches the alt attribute of a contained img element
133
- # @filter [String] :class Matches the class(es) provided
134
- # @filter [String, Regexp,nil] :href Matches the normalized href of the link, if nil will find <a> elements with no href attribute
135
- #
136
79
  Capybara.add_selector(:link) do
137
80
  xpath(:title, :alt) do |locator, href: true, enable_aria_label: false, alt: nil, title: nil, **_options|
138
81
  xpath = XPath.descendant(:a)
@@ -154,9 +97,9 @@ Capybara.add_selector(:link) do
154
97
  matchers = [XPath.attr(:id) == locator,
155
98
  XPath.string.n.is(locator),
156
99
  XPath.attr(:title).is(locator),
157
- XPath.descendant(:img)[XPath.attr(:alt).is(locator)]].reduce(:|)
158
- matchers |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
159
- xpath = xpath[matchers]
100
+ XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
101
+ matchers << XPath.attr(:'aria-label').is(locator) if enable_aria_label
102
+ xpath = xpath[matchers.reduce(:|)]
160
103
  end
161
104
 
162
105
  xpath = xpath[find_by_attr(:title, title)]
@@ -164,31 +107,20 @@ Capybara.add_selector(:link) do
164
107
  xpath
165
108
  end
166
109
 
167
- filter(:href) do |node, href|
110
+ node_filter(:href) do |node, href|
168
111
  # If not a Regexp it's been handled in the main XPath
169
112
  href.is_a?(Regexp) ? node[:href].match(href) : true
170
113
  end
171
114
 
172
115
  describe do |**options|
173
- desc = "".dup
116
+ desc = +""
174
117
  desc << " with href #{options[:href].inspect}" if options[:href]
175
118
  desc << " with no href attribute" if options.fetch(:href, true).nil?
176
119
  end
177
120
  end
178
121
 
179
- ##
180
- #
181
- # Find buttons ( input [of type submit, reset, image, button] or button elements )
182
- #
183
- # @locator Matches the id, value, or title attributes, string content of a button, or the alt attribute of an image type button or of a descendant image of a button
184
- #
185
- # @filter [String] :id Matches the id attribute
186
- # @filter [String] :title Matches the title attribute
187
- # @filter [String] :class Matches the class(es) provided
188
- # @filter [String] :value Matches the value of an input button
189
- #
190
122
  Capybara.add_selector(:button) do
191
- xpath(:value, :title, :type) do |locator, **options|
123
+ xpath(:value, :title, :type) do |locator, enable_aria_label: false, **options|
192
124
  input_btn_xpath = XPath.descendant(:input)[XPath.attr(:type).one_of('submit', 'reset', 'image', 'button')]
193
125
  btn_xpath = XPath.descendant(:button)
194
126
  image_btn_xpath = XPath.descendant(:input)[XPath.attr(:type) == 'image']
@@ -196,14 +128,14 @@ Capybara.add_selector(:button) do
196
128
  unless locator.nil?
197
129
  locator = locator.to_s
198
130
  locator_matches = XPath.attr(:id).equals(locator) | XPath.attr(:value).is(locator) | XPath.attr(:title).is(locator)
199
- locator_matches |= XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
131
+ locator_matches |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
200
132
 
201
133
  input_btn_xpath = input_btn_xpath[locator_matches]
202
134
 
203
135
  btn_xpath = btn_xpath[locator_matches | XPath.string.n.is(locator) | XPath.descendant(:img)[XPath.attr(:alt).is(locator)]]
204
136
 
205
137
  alt_matches = XPath.attr(:alt).is(locator)
206
- alt_matches |= XPath.attr(:'aria-label').is(locator) if options[:enable_aria_label]
138
+ alt_matches |= XPath.attr(:'aria-label').is(locator) if enable_aria_label
207
139
  image_btn_xpath = image_btn_xpath[alt_matches]
208
140
  end
209
141
 
@@ -214,45 +146,27 @@ Capybara.add_selector(:button) do
214
146
  res_xpath
215
147
  end
216
148
 
217
- filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
149
+ node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| !(value ^ node.disabled?) }
218
150
 
219
151
  describe do |disabled: nil, **options|
220
- desc = "".dup
152
+ desc = +""
221
153
  desc << " that is disabled" if disabled == true
222
154
  desc << describe_all_expression_filters(options)
223
155
  desc
224
156
  end
225
157
  end
226
158
 
227
- ##
228
- #
229
- # Find links or buttons
230
- #
231
159
  Capybara.add_selector(:link_or_button) do
232
160
  label "link or button"
233
161
  xpath do |locator, **options|
234
162
  self.class.all.values_at(:link, :button).map { |selector| selector.xpath.call(locator, options) }.reduce(:union)
235
163
  end
236
164
 
237
- filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| node.tag_name == "a" or !(value ^ node.disabled?) }
165
+ node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| node.tag_name == "a" || !(value ^ node.disabled?) }
238
166
 
239
167
  describe { |disabled: nil, **_options| " that is disabled" if disabled == true }
240
168
  end
241
169
 
242
- ##
243
- #
244
- # Find text fillable fields ( textarea, input [not of type submit, image, radio, checkbox, hidden, file] )
245
- #
246
- # @locator Matches against the id, name, or placeholder
247
- # @filter [String] :id Matches the id attribute
248
- # @filter [String] :name Matches the name attribute
249
- # @filter [String] :placeholder Matches the placeholder attribute
250
- # @filter [String] :with Matches the current value of the field
251
- # @filter [String] :type Matches the type attribute of the field or element type for 'textarea'
252
- # @filter [String, Array<String>] :class Matches the class(es) provided
253
- # @filter [Boolean] :disabled Match disabled field?
254
- # @filter [Boolean] :multiple Match fields that accept multiple values
255
- #
256
170
  Capybara.add_selector(:fillable_field) do
257
171
  label "field"
258
172
 
@@ -272,31 +186,18 @@ Capybara.add_selector(:fillable_field) do
272
186
 
273
187
  filter_set(:_field, %i[disabled multiple name placeholder])
274
188
 
275
- filter(:with) do |node, with|
189
+ node_filter(:with) do |node, with|
276
190
  with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
277
191
  end
278
192
 
279
193
  describe do |options|
280
- desc = "".dup
194
+ desc = +""
281
195
  desc << describe_all_expression_filters(options)
282
196
  desc << " with value #{options[:with].to_s.inspect}" if options.key?(:with)
283
197
  desc
284
198
  end
285
199
  end
286
200
 
287
- ##
288
- #
289
- # Find radio buttons
290
- #
291
- # @locator Match id, name, or associated label text
292
- # @filter [String] :id Matches the id attribute
293
- # @filter [String] :name Matches the name attribute
294
- # @filter [String, Array<String>] :class Matches the class(es) provided
295
- # @filter [Boolean] :checked Match checked fields?
296
- # @filter [Boolean] :unchecked Match unchecked fields?
297
- # @filter [Boolean] :disabled Match disabled field?
298
- # @filter [String] :option Match the value
299
- #
300
201
  Capybara.add_selector(:radio_button) do
301
202
  label "radio button"
302
203
 
@@ -307,29 +208,16 @@ Capybara.add_selector(:radio_button) do
307
208
 
308
209
  filter_set(:_field, %i[checked unchecked disabled name])
309
210
 
310
- filter(:option) { |node, value| node.value == value.to_s }
211
+ node_filter(:option) { |node, value| node.value == value.to_s }
311
212
 
312
213
  describe do |option: nil, **options|
313
- desc = "".dup
214
+ desc = +""
314
215
  desc << " with value #{option.inspect}" if option
315
216
  desc << describe_all_expression_filters(options)
316
217
  desc
317
218
  end
318
219
  end
319
220
 
320
- ##
321
- #
322
- # Find checkboxes
323
- #
324
- # @locator Match id, name, or associated label text
325
- # @filter [String] :id Matches the id attribute
326
- # @filter [String] :name Matches the name attribute
327
- # @filter [String, Array<String>] :class Matches the class(es) provided
328
- # @filter [Boolean] :checked Match checked fields?
329
- # @filter [Boolean] :unchecked Match unchecked fields?
330
- # @filter [Boolean] :disabled Match disabled field?
331
- # @filter [String] :option Match the value
332
- #
333
221
  Capybara.add_selector(:checkbox) do
334
222
  xpath do |locator, **options|
335
223
  xpath = XPath.descendant(:input)[XPath.attr(:type) == 'checkbox']
@@ -338,32 +226,16 @@ Capybara.add_selector(:checkbox) do
338
226
 
339
227
  filter_set(:_field, %i[checked unchecked disabled name])
340
228
 
341
- filter(:option) { |node, value| node.value == value.to_s }
229
+ node_filter(:option) { |node, value| node.value == value.to_s }
342
230
 
343
231
  describe do |option: nil, **options|
344
- desc = "".dup
232
+ desc = +""
345
233
  desc << " with value #{option.inspect}" if option
346
234
  desc << describe_all_expression_filters(options)
347
235
  desc
348
236
  end
349
237
  end
350
238
 
351
- ##
352
- #
353
- # Find select elements
354
- #
355
- # @locator Match id, name, placeholder, or associated label text
356
- # @filter [String] :id Matches the id attribute
357
- # @filter [String] :name Matches the name attribute
358
- # @filter [String] :placeholder Matches the placeholder attribute
359
- # @filter [String, Array<String>] :class Matches the class(es) provided
360
- # @filter [Boolean] :disabled Match disabled field?
361
- # @filter [Boolean] :multiple Match fields that accept multiple values
362
- # @filter [Array<String>] :options Exact match options
363
- # @filter [Array<String>] :with_options Partial match options
364
- # @filter [String, Array<String>] :selected Match the selection(s)
365
- # @filter [String, Array<String>] :with_selected Partial match the selection(s)
366
- #
367
239
  Capybara.add_selector(:select) do
368
240
  label "select box"
369
241
 
@@ -374,7 +246,7 @@ Capybara.add_selector(:select) do
374
246
 
375
247
  filter_set(:_field, %i[disabled multiple name placeholder])
376
248
 
377
- filter(:options) do |node, options|
249
+ node_filter(:options) do |node, options|
378
250
  actual = if node.visible?
379
251
  node.all(:xpath, './/option', wait: false).map(&:text)
380
252
  else
@@ -384,24 +256,23 @@ Capybara.add_selector(:select) do
384
256
  end
385
257
 
386
258
  expression_filter(:with_options) do |expr, options|
387
- options.each do |option|
388
- expr = expr[Capybara::Selector.all[:option].call(option)]
259
+ options.inject(expr) do |xpath, option|
260
+ xpath[Capybara::Selector.all[:option].call(option)]
389
261
  end
390
- expr
391
262
  end
392
263
 
393
- filter(:selected) do |node, selected|
264
+ node_filter(:selected) do |node, selected|
394
265
  actual = node.all(:xpath, './/option', visible: false, wait: false).select(&:selected?).map { |option| option.text(:all) }
395
266
  Array(selected).sort == actual.sort
396
267
  end
397
268
 
398
- filter(:with_selected) do |node, selected|
269
+ node_filter(:with_selected) do |node, selected|
399
270
  actual = node.all(:xpath, './/option', visible: false, wait: false).select(&:selected?).map { |option| option.text(:all) }
400
271
  (Array(selected) - actual).empty?
401
272
  end
402
273
 
403
274
  describe do |options: nil, with_options: nil, selected: nil, with_selected: nil, **opts|
404
- desc = "".dup
275
+ desc = +""
405
276
  desc << " with options #{options.inspect}" if options
406
277
  desc << " with at least options #{with_options.inspect}" if with_options
407
278
  desc << " with #{selected.inspect} selected" if selected
@@ -421,20 +292,19 @@ Capybara.add_selector(:datalist_input) do
421
292
 
422
293
  filter_set(:_field, %i[disabled name placeholder])
423
294
 
424
- filter(:options) do |node, options|
295
+ node_filter(:options) do |node, options|
425
296
  actual = node.find("//datalist[@id=#{node[:list]}]", visible: :all).all(:datalist_option, wait: false).map(&:value)
426
297
  options.sort == actual.sort
427
298
  end
428
299
 
429
300
  expression_filter(:with_options) do |expr, options|
430
- options.each do |option|
431
- expr = expr[XPath.attr(:list) == XPath.anywhere(:datalist)[Capybara::Selector.all[:datalist_option].call(option)].attr(:id)]
301
+ options.inject(expr) do |xpath, option|
302
+ xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[Capybara::Selector.all[:datalist_option].call(option)].attr(:id)]
432
303
  end
433
- expr
434
304
  end
435
305
 
436
306
  describe do |options: nil, with_options: nil, **opts|
437
- desc = "".dup
307
+ desc = +""
438
308
  desc << " with options #{options.inspect}" if options
439
309
  desc << " with at least options #{with_options.inspect}" if with_options
440
310
  desc << describe_all_expression_filters(opts)
@@ -442,14 +312,6 @@ Capybara.add_selector(:datalist_input) do
442
312
  end
443
313
  end
444
314
 
445
- ##
446
- #
447
- # Find option elements
448
- #
449
- # @locator Match text of option
450
- # @filter [Boolean] :disabled Match disabled option
451
- # @filter [Boolean] :selected Match selected option
452
- #
453
315
  Capybara.add_selector(:option) do
454
316
  xpath do |locator|
455
317
  xpath = XPath.descendant(:option)
@@ -457,11 +319,11 @@ Capybara.add_selector(:option) do
457
319
  xpath
458
320
  end
459
321
 
460
- filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
461
- filter(:selected, :boolean) { |node, value| !(value ^ node.selected?) }
322
+ node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
323
+ node_filter(:selected, :boolean) { |node, value| !(value ^ node.selected?) }
462
324
 
463
325
  describe do |**options|
464
- desc = "".dup
326
+ desc = +""
465
327
  desc << " that is#{' not' unless options[:disabled]} disabled" if options.key?(:disabled)
466
328
  desc << " that is#{' not' unless options[:selected]} selected" if options.key?(:selected)
467
329
  desc
@@ -478,26 +340,15 @@ Capybara.add_selector(:datalist_option) do
478
340
  xpath
479
341
  end
480
342
 
481
- filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
343
+ node_filter(:disabled, :boolean) { |node, value| !(value ^ node.disabled?) }
482
344
 
483
345
  describe do |**options|
484
- desc = "".dup
346
+ desc = +""
485
347
  desc << " that is#{' not' unless options[:disabled]} disabled" if options.key?(:disabled)
486
348
  desc
487
349
  end
488
350
  end
489
351
 
490
- ##
491
- #
492
- # Find file input elements
493
- #
494
- # @locator Match id, name, or associated label text
495
- # @filter [String] :id Matches the id attribute
496
- # @filter [String] :name Matches the name attribute
497
- # @filter [String, Array<String>] :class Matches the class(es) provided
498
- # @filter [Boolean] :disabled Match disabled field?
499
- # @filter [Boolean] :multiple Match field that accepts multiple values
500
- #
501
352
  Capybara.add_selector(:file_field) do
502
353
  label "file field"
503
354
  xpath do |locator, options|
@@ -508,19 +359,12 @@ Capybara.add_selector(:file_field) do
508
359
  filter_set(:_field, %i[disabled multiple name])
509
360
 
510
361
  describe do |**options|
511
- desc = "".dup
362
+ desc = +""
512
363
  desc << describe_all_expression_filters(options)
513
364
  desc
514
365
  end
515
366
  end
516
367
 
517
- ##
518
- #
519
- # Find label elements
520
- #
521
- # @locator Match id or text contents
522
- # @filter [Element, String] :for The element or id of the element associated with the label
523
- #
524
368
  Capybara.add_selector(:label) do
525
369
  label "label"
526
370
  xpath(:for) do |locator, options|
@@ -536,7 +380,7 @@ Capybara.add_selector(:label) do
536
380
  xpath
537
381
  end
538
382
 
539
- filter(:for) do |node, field_or_value|
383
+ node_filter(:for) do |node, field_or_value|
540
384
  if field_or_value.is_a? Capybara::Node::Element
541
385
  if node[:for]
542
386
  field_or_value[:id] == node[:for]
@@ -544,51 +388,32 @@ Capybara.add_selector(:label) do
544
388
  field_or_value.find_xpath('./ancestor::label[1]').include? node.base
545
389
  end
546
390
  else
547
- # Non element values were handled through the expression filter
548
- true
391
+ true # Non element values were handled through the expression filter
549
392
  end
550
393
  end
551
394
 
552
395
  describe do |**options|
553
- desc = "".dup
396
+ desc = +""
554
397
  desc << " for #{options[:for]}" if options[:for]
555
398
  desc
556
399
  end
557
400
  end
558
401
 
559
- ##
560
- #
561
- # Find table elements
562
- #
563
- # @locator id or caption text of table
564
- # @filter [String] :id Match id attribute of table
565
- # @filter [String] :caption Match text of associated caption
566
- # @filter [String, Array<String>] :class Matches the class(es) provided
567
- #
568
402
  Capybara.add_selector(:table) do
569
- xpath(:caption) do |locator, options|
403
+ xpath(:caption) do |locator, caption: nil, **_options|
570
404
  xpath = XPath.descendant(:table)
571
405
  xpath = xpath[(XPath.attr(:id) == locator.to_s) | XPath.descendant(:caption).is(locator.to_s)] unless locator.nil?
572
- xpath = xpath[XPath.descendant(:caption) == options[:caption]] if options[:caption]
406
+ xpath = xpath[XPath.descendant(:caption) == caption] if caption
573
407
  xpath
574
408
  end
575
409
 
576
410
  describe do |caption: nil, **_options|
577
- desc = "".dup
411
+ desc = +""
578
412
  desc << " with caption #{caption}" if caption
579
413
  desc
580
414
  end
581
415
  end
582
416
 
583
- ##
584
- #
585
- # Find frame/iframe elements
586
- #
587
- # @locator Match id or name
588
- # @filter [String] :id Match id attribute
589
- # @filter [String] :name Match name attribute
590
- # @filter [String, Array<String>] :class Matches the class(es) provided
591
- #
592
417
  Capybara.add_selector(:frame) do
593
418
  xpath(:name) do |locator, **options|
594
419
  xpath = XPath.descendant(:iframe).union(XPath.descendant(:frame))
@@ -598,11 +423,36 @@ Capybara.add_selector(:frame) do
598
423
  end
599
424
 
600
425
  describe do |name: nil, **_options|
601
- desc = "".dup
426
+ desc = +""
602
427
  desc << " with name #{name}" if name
603
428
  desc
604
429
  end
605
430
  end
606
431
 
432
+ Capybara.add_selector(:element) do
433
+ xpath do |locator, **_options|
434
+ XPath.descendant((locator || '@').to_sym)
435
+ end
436
+
437
+ expression_filter(:attributes, matcher: /.+/) do |xpath, name, val|
438
+ case val
439
+ when Regexp
440
+ xpath
441
+ when XPath::Expression
442
+ xpath[XPath.attr(name)[val]]
443
+ else
444
+ xpath[XPath.attr(name.to_sym) == val]
445
+ end
446
+ end
447
+
448
+ node_filter(:attributes, matcher: /.+/) do |node, name, val|
449
+ val.is_a?(Regexp) ? node[name] =~ val : true
450
+ end
451
+
452
+ describe do |**options|
453
+ desc = +""
454
+ desc << describe_all_expression_filters(options)
455
+ desc
456
+ end
457
+ end
607
458
  # rubocop:enable Metrics/BlockLength
608
- # rubocop:enable Metrics/ParameterLists