capybara 3.1.1 → 3.2.0

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