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
@@ -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)