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
@@ -50,18 +50,12 @@ module Capybara
50
50
  # ignored. This behaviour can be overridden by passing `:all` to this
51
51
  # method.
52
52
  #
53
- # @param [:all, :visible] type Whether to return only visible or all text
53
+ # @param type [:all, :visible] Whether to return only visible or all text
54
54
  # @return [String] The text of the element
55
55
  #
56
56
  def text(type = nil)
57
- type ||= :all unless session_options.ignore_hidden_elements or session_options.visible_text_only
58
- synchronize do
59
- if type == :all
60
- base.all_text
61
- else
62
- base.visible_text
63
- end
64
- end
57
+ type ||= :all unless session_options.ignore_hidden_elements || session_options.visible_text_only
58
+ synchronize { type == :all ? base.all_text : base.visible_text }
65
59
  end
66
60
 
67
61
  ##
@@ -90,11 +84,12 @@ module Capybara
90
84
  # Set the value of the form element to the given value.
91
85
  #
92
86
  # @param [String] value The new value
93
- # @param [Hash{}] options Driver specific options for how to set the value
87
+ # @param [Hash{}] options Driver specific options for how to set the value. Take default values from {Capybara#default_set_options}
94
88
  #
95
89
  # @return [Capybara::Node::Element] The element
96
90
  def set(value, **options)
97
91
  raise Capybara::ReadOnlyElementError, "Attempt to set readonly element with value: #{value}" if readonly?
92
+ options = session_options.default_set_options.to_h.merge(options)
98
93
  synchronize { base.set(value, options) }
99
94
  self
100
95
  end
@@ -125,9 +120,11 @@ module Capybara
125
120
  # Click the Element
126
121
  #
127
122
  # @!macro click_modifiers
128
- # @overload $0(*key_modifiers=[], offset={x: nil, y: nil})
129
- # @param [Array<:alt, :control, :meta, :shift>] *key_modifiers Keys to be held down when clicking
130
- # @param [Hash] offset x and y coordinates to offset the click location from the top left corner of the element. If not specified will click the middle of the element.
123
+ # Both x: and y: must be specified if an offset is wanted, if not specified the click will occur at the middle of the element
124
+ # @overload $0(*modifier_keys, **offset)
125
+ # @param *modifier_keys [:alt, :control, :meta, :shift] ([]) Keys to be held down when clicking
126
+ # @option offset [Integer] x X coordinate to offset the click location from the top left corner of the element
127
+ # @option offset [Integer] y Y coordinate to offset the click location from the top left corner of the element
131
128
  # @return [Capybara::Node::Element] The element
132
129
  def click(*keys, **offset)
133
130
  synchronize { base.click(keys, offset) }
@@ -161,7 +158,7 @@ module Capybara
161
158
  # Send Keystrokes to the Element
162
159
  #
163
160
  # @overload send_keys(keys, ...)
164
- # @param [String, Symbol, Array<String,Symbol>] keys
161
+ # @param keys [String, Symbol, Array<String,Symbol>]
165
162
  #
166
163
  # Examples:
167
164
  #
@@ -350,12 +347,46 @@ module Capybara
350
347
  self
351
348
  end
352
349
 
350
+ ##
351
+ #
352
+ # Execute the given JS in the context of the element not returning a result. This is useful for scripts that return
353
+ # complex objects, such as jQuery statements. +execute_script+ should be used over
354
+ # +evaluate_script+ whenever possible. `this` in the script will refer to the element this is called on.
355
+ #
356
+ # @param [String] script A string of JavaScript to execute
357
+ # @param args Optional arguments that will be passed to the script. Driver support for this is optional and types of objects supported may differ between drivers
358
+ #
359
+ def execute_script(script, *args)
360
+ session.execute_script(<<~JS, self, *args)
361
+ (function (){
362
+ #{script}
363
+ }).apply(arguments[0], Array.prototype.slice.call(arguments,1));
364
+ JS
365
+ end
366
+
367
+ ##
368
+ #
369
+ # Evaluate the given JS in the context of the element and return the result. Be careful when using this with
370
+ # scripts that return complex objects, such as jQuery statements. +execute_script+ might
371
+ # be a better alternative. `this` in the script will refer to the element this is called on.
372
+ #
373
+ # @param [String] script A string of JavaScript to evaluate
374
+ # @return [Object] The result of the evaluated JavaScript (may be driver specific)
375
+ #
376
+ def evaluate_script(script, *args)
377
+ session.evaluate_script(<<~JS, self, *args)
378
+ (function(){
379
+ return #{script}
380
+ }).apply(arguments[0], Array.prototype.slice.call(arguments,1));
381
+ JS
382
+ end
383
+
353
384
  def reload
354
385
  if @allow_reload
355
386
  begin
356
387
  reloaded = query_scope.reload.first(@query.name, @query.locator, @query.options)
357
388
  @base = reloaded.base if reloaded
358
- rescue => e
389
+ rescue StandardError => e
359
390
  raise e unless catch_error?(e)
360
391
  end
361
392
  end
@@ -366,7 +397,7 @@ module Capybara
366
397
  %(#<Capybara::Node::Element tag="#{base.tag_name}" path="#{base.path}">)
367
398
  rescue NotSupportedByDriverError
368
399
  %(#<Capybara::Node::Element tag="#{base.tag_name}">)
369
- rescue => e
400
+ rescue StandardError => e
370
401
  raise unless session.driver.invalid_element_errors.any? { |et| e.is_a?(et) }
371
402
 
372
403
  %(Obsolete #<Capybara::Node::Element>)
@@ -55,7 +55,7 @@ module Capybara
55
55
  #
56
56
  def ancestor(*args, **options, &optional_filter_block)
57
57
  options[:session_options] = session_options
58
- synced_resolve Capybara::Queries::AncestorQuery.new(*args, **options, &optional_filter_block)
58
+ synced_resolve Capybara::Queries::AncestorQuery.new(*args, options, &optional_filter_block)
59
59
  end
60
60
 
61
61
  ##
@@ -81,14 +81,14 @@ module Capybara
81
81
  #
82
82
  def sibling(*args, **options, &optional_filter_block)
83
83
  options[:session_options] = session_options
84
- synced_resolve Capybara::Queries::SiblingQuery.new(*args, **options, &optional_filter_block)
84
+ synced_resolve Capybara::Queries::SiblingQuery.new(*args, options, &optional_filter_block)
85
85
  end
86
86
 
87
87
  ##
88
88
  #
89
89
  # Find a form field on the page. The field can be found by its name, id or label text.
90
90
  #
91
- # @overload find_field([locator], options={})
91
+ # @overload find_field([locator], **options)
92
92
  # @param [String] locator name, id, placeholder or text of associated label element
93
93
  #
94
94
  # @macro waiting_behavior
@@ -119,7 +119,7 @@ module Capybara
119
119
  #
120
120
  # Find a link on the page. The link can be found by its id or text.
121
121
  #
122
- # @overload find_link([locator], options={})
122
+ # @overload find_link([locator], **options)
123
123
  # @param [String] locator id, title, text, or alt of enclosed img element
124
124
  #
125
125
  # @macro waiting_behavior
@@ -142,10 +142,10 @@ module Capybara
142
142
  # \<button> element. All buttons can be found by their id, value, or title. \<button> elements can also be found
143
143
  # by their text content, and image \<input> elements by their alt attribute
144
144
  #
145
- # @overload find_button([locator], options={})
145
+ # @overload find_button([locator], **options)
146
146
  # @param [String] locator id, value, title, text content, alt of image
147
147
  #
148
- # @overload find_button(options={})
148
+ # @overload find_button(**options)
149
149
  #
150
150
  # @macro waiting_behavior
151
151
  #
@@ -178,7 +178,7 @@ module Capybara
178
178
  end
179
179
 
180
180
  ##
181
- # @!method all([kind = Capybara.default_selector], locator = nil, options = {})
181
+ # @!method all([kind = Capybara.default_selector], locator = nil, **options)
182
182
  #
183
183
  # Find all elements on the page matching the given selector
184
184
  # and options.
@@ -235,8 +235,8 @@ module Capybara
235
235
  # @option options [Range] between Number of matches found must be within the given range
236
236
  # @option options [Boolean] exact Control whether `is` expressions in the given XPath match exactly or partially
237
237
  # @option options [Integer, false] wait (Capybara.default_max_wait_time) The time to wait for matching elements to become available
238
- # @overload all([kind = Capybara.default_selector], locator = nil, options = {})
239
- # @overload all([kind = Capybara.default_selector], locator = nil, options = {}, &filter_block)
238
+ # @overload all([kind = Capybara.default_selector], locator = nil, **options)
239
+ # @overload all([kind = Capybara.default_selector], locator = nil, **options, &filter_block)
240
240
  # @yieldparam element [Capybara::Node::Element] The element being considered for inclusion in the results
241
241
  # @yieldreturn [Boolean] Should the element be considered in the results?
242
242
  # @return [Capybara::Result] A collection of found elements
@@ -245,7 +245,7 @@ module Capybara
245
245
  minimum_specified = options_include_minimum?(options)
246
246
  options = { minimum: 1 }.merge(options) unless minimum_specified
247
247
  options[:session_options] = session_options
248
- query = Capybara::Queries::SelectorQuery.new(*args.push(options), &optional_filter_block)
248
+ query = Capybara::Queries::SelectorQuery.new(*args, options, &optional_filter_block)
249
249
  result = nil
250
250
  begin
251
251
  synchronize(query.wait) do
@@ -276,7 +276,7 @@ module Capybara
276
276
  #
277
277
  def first(*args, **options, &optional_filter_block)
278
278
  options = { minimum: 1 }.merge(options) unless options_include_minimum?(options)
279
- all(*args, **options, &optional_filter_block).first
279
+ all(*args, options, &optional_filter_block).first
280
280
  end
281
281
 
282
282
  private
@@ -298,11 +298,11 @@ module Capybara
298
298
  end
299
299
 
300
300
  def ambiguous?(query, result)
301
- query.match == :one or query.match == :smart and result.size > 1
301
+ %i[one smart].include?(query.match) && (result.size > 1)
302
302
  end
303
303
 
304
304
  def prefer_exact?(query)
305
- query.match == :smart or query.match == :prefer_exact
305
+ %i[smart prefer_exact].include?(query.match)
306
306
  end
307
307
 
308
308
  def options_include_minimum?(opts)
@@ -91,7 +91,7 @@ module Capybara
91
91
  #
92
92
  def assert_selector(*args, &optional_filter_block)
93
93
  _verify_selector_result(args, optional_filter_block) do |result, query|
94
- unless result.matches_count? && (!result.empty? || query.expects_none?)
94
+ unless result.matches_count? && (result.any? || query.expects_none?)
95
95
  raise Capybara::ExpectationNotMet, result.failure_message
96
96
  end
97
97
  end
@@ -110,11 +110,11 @@ module Capybara
110
110
  # The :wait option applies to all of the selectors as a group, so all of the locators must be present
111
111
  # within :wait (Defaults to Capybara.default_max_wait_time) seconds.
112
112
  #
113
- # @overload assert_all_of_selectors([kind = Capybara.default_selector], *locators, options = {})
113
+ # @overload assert_all_of_selectors([kind = Capybara.default_selector], *locators, **options)
114
114
  #
115
115
  def assert_all_of_selectors(*args, wait: nil, **options, &optional_filter_block)
116
116
  wait = session_options.default_max_wait_time if wait.nil?
117
- selector = args.first.is_a?(Symbol) ? args.shift : session_options.default_selector
117
+ selector = extract_selector(args)
118
118
  synchronize(wait) do
119
119
  args.each do |locator|
120
120
  assert_selector(selector, locator, options, &optional_filter_block)
@@ -135,11 +135,11 @@ module Capybara
135
135
  # The :wait option applies to all of the selectors as a group, so none of the locators must be present
136
136
  # within :wait (Defaults to Capybara.default_max_wait_time) seconds.
137
137
  #
138
- # @overload assert_none_of_selectors([kind = Capybara.default_selector], *locators, options = {})
138
+ # @overload assert_none_of_selectors([kind = Capybara.default_selector], *locators, **options)
139
139
  #
140
140
  def assert_none_of_selectors(*args, wait: nil, **options, &optional_filter_block)
141
141
  wait = session_options.default_max_wait_time if wait.nil?
142
- selector = args.first.is_a?(Symbol) ? args.shift : session_options.default_selector
142
+ selector = extract_selector(args)
143
143
  synchronize(wait) do
144
144
  args.each do |locator|
145
145
  assert_no_selector(selector, locator, options, &optional_filter_block)
@@ -508,7 +508,7 @@ module Capybara
508
508
  def matches_selector?(*args, &optional_filter_block)
509
509
  assert_matches_selector(*args, &optional_filter_block)
510
510
  rescue Capybara::ExpectationNotMet
511
- return false
511
+ false
512
512
  end
513
513
 
514
514
  ##
@@ -544,7 +544,7 @@ module Capybara
544
544
  def not_matches_selector?(*args, &optional_filter_block)
545
545
  assert_not_matches_selector(*args, &optional_filter_block)
546
546
  rescue Capybara::ExpectationNotMet
547
- return false
547
+ false
548
548
  end
549
549
 
550
550
  ##
@@ -574,7 +574,7 @@ module Capybara
574
574
  # ignoring any HTML tags.
575
575
  #
576
576
  # @!macro text_query_params
577
- # @overload $0(type, text, options = {})
577
+ # @overload $0(type, text, **options)
578
578
  # @param [:all, :visible] type Whether to check for only visible or all text. If this parameter is missing or nil then we use the value of `Capybara.ignore_hidden_elements`, which defaults to `true`, corresponding to `:visible`.
579
579
  # @param [String, Regexp] text The string/regexp to check for. If it's a string, text is expected to include it. If it's a regexp, text is expected to match it.
580
580
  # @option options [Integer] :count (nil) Number of times the text is expected to occur
@@ -583,7 +583,7 @@ module Capybara
583
583
  # @option options [Range] :between (nil) Range of times that is expected to contain number of times text occurs
584
584
  # @option options [Numeric] :wait (Capybara.default_max_wait_time) Maximum time that Capybara will wait for text to eq/match given string/regexp argument
585
585
  # @option options [Boolean] :exact (Capybara.exact_text) Whether text must be an exact match or just substring
586
- # @overload $0(text, options = {})
586
+ # @overload $0(text, **options)
587
587
  # @param [String, Regexp] text The string/regexp to check for. If it's a string, text is expected to include it. If it's a regexp, text is expected to match it.
588
588
  # @option options [Integer] :count (nil) Number of times the text is expected to occur
589
589
  # @option options [Integer] :minimum (nil) Minimum number of times the text is expected to occur
@@ -596,7 +596,7 @@ module Capybara
596
596
  #
597
597
  def assert_text(*args)
598
598
  _verify_text(args) do |count, query|
599
- unless query.matches_count?(count) && ((count > 0) || query.expects_none?)
599
+ unless query.matches_count?(count) && (count.positive? || query.expects_none?)
600
600
  raise Capybara::ExpectationNotMet, query.failure_message
601
601
  end
602
602
  end
@@ -612,7 +612,7 @@ module Capybara
612
612
  #
613
613
  def assert_no_text(*args)
614
614
  _verify_text(args) do |count, query|
615
- if query.matches_count?(count) && ((count > 0) || query.expects_none?)
615
+ if query.matches_count?(count) && (count.positive? || query.expects_none?)
616
616
  raise Capybara::ExpectationNotMet, query.negative_failure_message
617
617
  end
618
618
  end
@@ -659,12 +659,15 @@ module Capybara
659
659
 
660
660
  private
661
661
 
662
+ def extract_selector(args)
663
+ args.first.is_a?(Symbol) ? args.shift : session_options.default_selector
664
+ end
665
+
662
666
  def _verify_selector_result(query_args, optional_filter_block)
663
667
  query_args = _set_query_session_options(*query_args)
664
668
  query = Capybara::Queries::SelectorQuery.new(*query_args, &optional_filter_block)
665
669
  synchronize(query.wait) do
666
- result = query.resolve_for(self)
667
- yield result, query
670
+ yield query.resolve_for(self), query
668
671
  end
669
672
  true
670
673
  end
@@ -673,8 +676,7 @@ module Capybara
673
676
  query_args = _set_query_session_options(*query_args)
674
677
  query = Capybara::Queries::MatchQuery.new(*query_args, &optional_filter_block)
675
678
  synchronize(query.wait) do
676
- result = query.resolve_for(query_scope)
677
- yield result
679
+ yield query.resolve_for(query_scope)
678
680
  end
679
681
  true
680
682
  end
@@ -683,8 +685,7 @@ module Capybara
683
685
  query_args = _set_query_session_options(*query_args)
684
686
  query = Capybara::Queries::TextQuery.new(*query_args)
685
687
  synchronize(query.wait) do
686
- count = query.resolve_for(self)
687
- yield(count, query)
688
+ yield query.resolve_for(self), query
688
689
  end
689
690
  true
690
691
  end
@@ -45,7 +45,7 @@ module Capybara
45
45
  attr_name = name.to_s
46
46
  if attr_name == 'value'
47
47
  value
48
- elsif tag_name == 'input' and native[:type] == 'checkbox' and attr_name == 'checked'
48
+ elsif (tag_name == 'input') && (native[:type] == 'checkbox') && (attr_name == 'checked')
49
49
  native['checked'] == 'checked'
50
50
  else
51
51
  native[attr_name]
@@ -78,7 +78,7 @@ module Capybara
78
78
  if tag_name == 'textarea'
79
79
  native['_capybara_raw_value']
80
80
  elsif tag_name == 'select'
81
- if native['multiple'] == 'multiple'
81
+ if multiple?
82
82
  native.xpath(".//option[@selected='selected']").map { |option| option[:value] || option.content }
83
83
  else
84
84
  option = native.xpath(".//option[@selected='selected']").first || native.xpath(".//option").first
@@ -139,6 +139,10 @@ module Capybara
139
139
  native.has_attribute?('selected')
140
140
  end
141
141
 
142
+ def multiple?
143
+ native.has_attribute?('multiple')
144
+ end
145
+
142
146
  def synchronize(_seconds = nil)
143
147
  yield # simple nodes don't need to wait
144
148
  end
@@ -13,7 +13,7 @@ module Capybara
13
13
  end
14
14
 
15
15
  def description
16
- child_query = @child_node && @child_node.instance_variable_get(:@query)
16
+ child_query = @child_node&.instance_variable_get(:@query)
17
17
  desc = super
18
18
  desc += " that is an ancestor of #{child_query.description}" if child_query
19
19
  desc
@@ -59,11 +59,11 @@ module Capybara
59
59
  # Generates a failure message from the query description and count options.
60
60
  #
61
61
  def failure_message
62
- String.new("expected to find #{description}") << count_message
62
+ +"expected to find #{description}" << count_message
63
63
  end
64
64
 
65
65
  def negative_failure_message
66
- String.new("expected not to find #{description}") << count_message
66
+ +"expected not to find #{description}" << count_message
67
67
  end
68
68
 
69
69
  private
@@ -73,7 +73,7 @@ module Capybara
73
73
  end
74
74
 
75
75
  def count_message
76
- message = "".dup
76
+ message = +""
77
77
  if options[:count]
78
78
  message << " #{options[:count]} #{Capybara::Helpers.declension('time', 'times', options[:count])}"
79
79
  elsif options[:between]
@@ -19,7 +19,7 @@ module Capybara
19
19
  def resolves_for?(session)
20
20
  uri = ::Addressable::URI.parse(session.current_url)
21
21
  uri.query = nil if uri && options[:ignore_query]
22
- @actual_path = options[:url] ? uri.to_s : uri && uri.request_uri
22
+ @actual_path = options[:url] ? uri.to_s : uri&.request_uri
23
23
 
24
24
  if @expected_path.is_a? Regexp
25
25
  @actual_path.to_s.match(@expected_path)
@@ -9,6 +9,14 @@ module Capybara
9
9
 
10
10
  private
11
11
 
12
+ def assert_valid_keys
13
+ invalid_options = @options.keys & COUNT_KEYS
14
+ unless invalid_options.empty?
15
+ raise ArgumentError, "Match queries don't support quantity options. Invalid keys - #{invalid_options.join(', ')}"
16
+ end
17
+ super
18
+ end
19
+
12
20
  def valid_keys
13
21
  super - COUNT_KEYS
14
22
  end
@@ -31,12 +31,12 @@ module Capybara
31
31
  def label; selector.label || selector.name; end
32
32
 
33
33
  def description
34
- @description = "".dup
34
+ @description = +""
35
35
  @description << "visible " if visible == :visible
36
36
  @description << "non-visible " if visible == :hidden
37
37
  @description << "#{label} #{locator.inspect}"
38
38
  @description << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
39
- @description << " with exact text #{options[:exact_text]}" if options[:exact_text].is_a?(String)
39
+ @description << " with exact text #{exact_text}" if exact_text.is_a?(String)
40
40
  @description << " with id #{options[:id]}" if options[:id]
41
41
  @description << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
42
42
  @description << selector.description(options)
@@ -56,7 +56,7 @@ module Capybara
56
56
 
57
57
  matches_node_filters?(node) && matches_filter_block?(node)
58
58
  rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
59
- return false
59
+ false
60
60
  end
61
61
 
62
62
  def visible
@@ -126,11 +126,19 @@ module Capybara
126
126
  end
127
127
 
128
128
  def matches_node_filters?(node)
129
- node_filters.all? do |name, filter|
130
- if options.key?(name)
131
- filter.matches?(node, options[name])
129
+ unapplied_options = options.keys - valid_keys
130
+
131
+ node_filters.all? do |filter_name, filter|
132
+ if filter.matcher?
133
+ unapplied_options.select { |option_name| filter.handles_option?(option_name) }.all? do |option_name|
134
+ unapplied_options.delete(option_name)
135
+ filter.matches?(node, option_name, options[option_name])
136
+ end
137
+ elsif options.key?(filter_name)
138
+ unapplied_options.delete(filter_name)
139
+ filter.matches?(node, filter_name, options[filter_name])
132
140
  elsif filter.default?
133
- filter.matches?(node, filter.default)
141
+ filter.matches?(node, filter_name, filter.default)
134
142
  else
135
143
  true
136
144
  end
@@ -166,43 +174,86 @@ module Capybara
166
174
  end
167
175
 
168
176
  def assert_valid_keys
169
- super
170
- return if VALID_MATCH.include?(match)
171
- raise ArgumentError, "invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(', ')}"
177
+ unless VALID_MATCH.include?(match)
178
+ raise ArgumentError, "invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(', ')}"
179
+ end
180
+ unhandled_options = @options.keys - valid_keys
181
+ unhandled_options -= @options.keys.select do |option_name|
182
+ expression_filters.any? { |_nmae, ef| ef.handles_option? option_name } ||
183
+ node_filters.any? { |_name, nf| nf.handles_option? option_name }
184
+ end
185
+
186
+ return if unhandled_options.empty?
187
+ invalid_names = unhandled_options.map(&:inspect).join(", ")
188
+ valid_names = valid_keys.map(&:inspect).join(", ")
189
+ raise ArgumentError, "invalid keys #{invalid_names}, should be one of #{valid_names}"
172
190
  end
173
191
 
174
192
  def filtered_xpath(expr)
175
- if options.key?(:id) || options.key?(:class)
176
- expr = "(#{expr})"
177
- expr = "#{expr}[#{XPath.attr(:id) == options[:id]}]" if options.key?(:id) && !custom_keys.include?(:id)
178
- if options.key?(:class) && !custom_keys.include?(:class)
179
- class_xpath = Array(options[:class]).map do |klass|
180
- XPath.attr(:class).contains_word(klass)
193
+ if options.key?(:id) && !custom_keys.include?(:id)
194
+ expr = if options[:id].is_a? XPath::Expression
195
+ "(#{expr})[#{XPath.attr(:id)[options[:id]]}]"
196
+ else
197
+ "(#{expr})[#{XPath.attr(:id) == options[:id]}]"
198
+ end
199
+ end
200
+ if options.key?(:class) && !custom_keys.include?(:class)
201
+ class_xpath = if options[:class].is_a?(XPath::Expression)
202
+ XPath.attr(:class)[options[:class]]
203
+ else
204
+ Array(options[:class]).map do |klass|
205
+ if klass.start_with?('!')
206
+ !XPath.attr(:class).contains_word(klass.slice(1))
207
+ else
208
+ XPath.attr(:class).contains_word(klass)
209
+ end
181
210
  end.reduce(:&)
182
- expr = "#{expr}[#{class_xpath}]"
183
211
  end
212
+ expr = "(#{expr})[#{class_xpath}]"
184
213
  end
185
214
  expr
186
215
  end
187
216
 
188
217
  def filtered_css(expr)
189
- if options.key?(:id) || options.key?(:class)
190
- css_selectors = expr.split(',').map(&:rstrip)
191
- expr = css_selectors.map do |sel|
192
- sel += "##{Capybara::Selector::CSS.escape(options[:id])}" if options.key?(:id) && !custom_keys.include?(:id)
193
- sel += Array(options[:class]).map { |k| ".#{Capybara::Selector::CSS.escape(k)}" }.join if options.key?(:class) && !custom_keys.include?(:class)
194
- sel
195
- end.join(", ")
218
+ process_id = options.key?(:id) && !custom_keys.include?(:id)
219
+ process_class = options.key?(:class) && !custom_keys.include?(:class)
220
+
221
+ if process_id && options[:id].is_a?(XPath::Expression)
222
+ raise ArgumentError, "XPath expressions are not supported for the :id filter with CSS based selectors"
223
+ end
224
+ if process_class && options[:class].is_a?(XPath::Expression)
225
+ raise ArgumentError, "XPath expressions are not supported for the :class filter with CSS based selectors"
196
226
  end
227
+
228
+ css_selectors = expr.split(',').map(&:rstrip)
229
+ expr = css_selectors.map do |sel|
230
+ sel += "##{Capybara::Selector::CSS.escape(options[:id])}" if process_id
231
+ sel += css_from_classes(Array(options[:class])) if process_class
232
+ sel
233
+ end.join(", ")
197
234
  expr
198
235
  end
199
236
 
237
+ def css_from_classes(classes)
238
+ classes = classes.group_by { |c| c.start_with? '!' }
239
+ (classes[false].to_a.map { |c| ".#{Capybara::Selector::CSS.escape(c)}" } +
240
+ classes[true].to_a.map { |c| ":not(.#{Capybara::Selector::CSS.escape(c.slice(1))})" }).join
241
+ end
242
+
200
243
  def apply_expression_filters(expr)
244
+ unapplied_options = options.keys - valid_keys
201
245
  expression_filters.inject(expr) do |memo, (name, ef)|
202
- if options.key?(name)
203
- ef.apply_filter(memo, options[name])
246
+ if ef.matcher?
247
+ unapplied_options.select { |option_name| ef.handles_option?(option_name) }.each do |option_name|
248
+ unapplied_options.delete(option_name)
249
+ memo = ef.apply_filter(memo, option_name, options[option_name])
250
+ end
251
+ memo
252
+ elsif options.key?(name)
253
+ unapplied_options.delete(name)
254
+ ef.apply_filter(memo, name, options[name])
204
255
  elsif ef.default?
205
- ef.apply_filter(memo, ef.default)
256
+ ef.apply_filter(memo, name, ef.default)
206
257
  else
207
258
  memo
208
259
  end
@@ -219,25 +270,29 @@ module Capybara
219
270
  end
220
271
 
221
272
  def describe_within?
222
- @resolved_node && !(@resolved_node.is_a?(::Capybara::Node::Document) ||
223
- (@resolved_node.is_a?(::Capybara::Node::Simple) && @resolved_node.path == '/'))
273
+ @resolved_node && !document?(@resolved_node) && !simple_root?(@resolved_node)
224
274
  end
225
275
 
226
- def matches_text_filter(node, text_option)
227
- regexp = if text_option.is_a?(Regexp)
228
- text_option
229
- elsif exact_text == true
230
- /\A#{Regexp.escape(text_option.to_s)}\z/
231
- else
232
- Regexp.escape(text_option.to_s)
233
- end
234
- text_visible = visible
235
- text_visible = :all if text_visible == :hidden
236
- node.text(text_visible).match(regexp)
276
+ def document?(node)
277
+ node.is_a?(::Capybara::Node::Document)
278
+ end
279
+
280
+ def simple_root?(node)
281
+ node.is_a?(::Capybara::Node::Simple) && node.path == '/'
282
+ end
283
+
284
+ def matches_text_filter(node, value)
285
+ return matches_exact_text_filter(node, value) if exact_text == true
286
+ regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
287
+ matches_text_regexp(node, regexp)
288
+ end
289
+
290
+ def matches_exact_text_filter(node, value)
291
+ regexp = value.is_a?(Regexp) ? value : /\A#{Regexp.escape(value.to_s)}\z/
292
+ matches_text_regexp(node, regexp)
237
293
  end
238
294
 
239
- def matches_exact_text_filter(node, exact_text_option)
240
- regexp = /\A#{Regexp.escape(exact_text_option)}\z/
295
+ def matches_text_regexp(node, regexp)
241
296
  text_visible = visible
242
297
  text_visible = :all if text_visible == :hidden
243
298
  node.text(text_visible).match(regexp)