capybara 3.1.1 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/History.md +19 -0
- data/README.md +1 -1
- data/lib/capybara.rb +2 -0
- data/lib/capybara/config.rb +2 -1
- data/lib/capybara/driver/base.rb +1 -1
- data/lib/capybara/driver/node.rb +3 -3
- data/lib/capybara/node/actions.rb +90 -92
- data/lib/capybara/node/base.rb +2 -2
- data/lib/capybara/node/document_matchers.rb +5 -5
- data/lib/capybara/node/element.rb +47 -16
- data/lib/capybara/node/finders.rb +13 -13
- data/lib/capybara/node/matchers.rb +18 -17
- data/lib/capybara/node/simple.rb +6 -2
- data/lib/capybara/queries/ancestor_query.rb +1 -1
- data/lib/capybara/queries/base_query.rb +3 -3
- data/lib/capybara/queries/current_path_query.rb +1 -1
- data/lib/capybara/queries/match_query.rb +8 -0
- data/lib/capybara/queries/selector_query.rb +97 -42
- data/lib/capybara/queries/sibling_query.rb +1 -1
- data/lib/capybara/queries/text_query.rb +12 -7
- data/lib/capybara/rack_test/browser.rb +9 -7
- data/lib/capybara/rack_test/form.rb +15 -17
- data/lib/capybara/rack_test/node.rb +12 -12
- data/lib/capybara/result.rb +26 -15
- data/lib/capybara/rspec.rb +1 -2
- data/lib/capybara/rspec/compound.rb +4 -4
- data/lib/capybara/rspec/matchers.rb +2 -2
- data/lib/capybara/selector.rb +75 -225
- data/lib/capybara/selector/css.rb +2 -2
- data/lib/capybara/selector/filter_set.rb +17 -21
- data/lib/capybara/selector/filters/base.rb +24 -1
- data/lib/capybara/selector/filters/expression_filter.rb +3 -5
- data/lib/capybara/selector/filters/node_filter.rb +4 -4
- data/lib/capybara/selector/selector.rb +221 -69
- data/lib/capybara/selenium/driver.rb +15 -88
- data/lib/capybara/selenium/node.rb +25 -28
- data/lib/capybara/server.rb +10 -54
- data/lib/capybara/server/animation_disabler.rb +43 -0
- data/lib/capybara/server/middleware.rb +55 -0
- data/lib/capybara/session.rb +29 -30
- data/lib/capybara/session/config.rb +11 -1
- data/lib/capybara/session/matchers.rb +5 -5
- data/lib/capybara/spec/session/assert_text_spec.rb +1 -1
- data/lib/capybara/spec/session/body_spec.rb +10 -12
- data/lib/capybara/spec/session/click_link_spec.rb +3 -3
- data/lib/capybara/spec/session/element/assert_match_selector_spec.rb +1 -1
- data/lib/capybara/spec/session/fill_in_spec.rb +9 -0
- data/lib/capybara/spec/session/find_field_spec.rb +1 -1
- data/lib/capybara/spec/session/find_spec.rb +8 -3
- data/lib/capybara/spec/session/has_link_spec.rb +2 -2
- data/lib/capybara/spec/session/node_spec.rb +50 -0
- data/lib/capybara/spec/session/node_wrapper_spec.rb +5 -5
- data/lib/capybara/spec/session/save_and_open_screenshot_spec.rb +1 -1
- data/lib/capybara/spec/session/window/windows_spec.rb +3 -5
- data/lib/capybara/spec/spec_helper.rb +4 -2
- data/lib/capybara/spec/views/with_animation.erb +46 -0
- data/lib/capybara/version.rb +1 -1
- data/lib/capybara/window.rb +3 -2
- data/spec/filter_set_spec.rb +19 -2
- data/spec/result_spec.rb +33 -1
- data/spec/rspec/features_spec.rb +6 -10
- data/spec/rspec/shared_spec_matchers.rb +4 -4
- data/spec/selector_spec.rb +74 -4
- data/spec/selenium_spec_marionette.rb +2 -0
- data/spec/server_spec.rb +1 -1
- data/spec/session_spec.rb +12 -0
- data/spec/shared_selenium_session.rb +30 -0
- metadata +8 -9
- data/.yard/templates_custom/default/class/html/selectors.erb +0 -38
- data/.yard/templates_custom/default/class/html/setup.rb +0 -17
- data/.yard/yard_extensions.rb +0 -78
- 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]
|
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
|
58
|
-
synchronize
|
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
|
-
#
|
129
|
-
#
|
130
|
-
# @param [
|
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>]
|
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,
|
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,
|
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
|
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
|
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,
|
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
|
-
|
301
|
+
%i[one smart].include?(query.match) && (result.size > 1)
|
302
302
|
end
|
303
303
|
|
304
304
|
def prefer_exact?(query)
|
305
|
-
|
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? && (
|
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
|
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
|
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
|
-
|
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
|
-
|
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) && (
|
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) && (
|
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
|
-
|
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
|
-
|
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
|
-
|
687
|
-
yield(count, query)
|
688
|
+
yield query.resolve_for(self), query
|
688
689
|
end
|
689
690
|
true
|
690
691
|
end
|
data/lib/capybara/node/simple.rb
CHANGED
@@ -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'
|
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
|
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
|
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
|
-
|
62
|
+
+"expected to find #{description}" << count_message
|
63
63
|
end
|
64
64
|
|
65
65
|
def negative_failure_message
|
66
|
-
|
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 = ""
|
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
|
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 = ""
|
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 #{
|
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
|
-
|
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
|
-
|
130
|
-
|
131
|
-
|
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
|
-
|
170
|
-
|
171
|
-
|
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)
|
176
|
-
expr =
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
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
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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
|
203
|
-
ef.
|
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
|
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
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
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
|
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)
|