capybara 3.9.0 → 3.10.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +15 -0
  3. data/License.txt +1 -1
  4. data/README.md +1 -1
  5. data/lib/capybara.rb +1 -2
  6. data/lib/capybara/helpers.rb +3 -2
  7. data/lib/capybara/minitest.rb +1 -1
  8. data/lib/capybara/minitest/spec.rb +1 -0
  9. data/lib/capybara/node/finders.rb +20 -15
  10. data/lib/capybara/node/matchers.rb +43 -10
  11. data/lib/capybara/queries/current_path_query.rb +2 -2
  12. data/lib/capybara/queries/selector_query.rb +9 -2
  13. data/lib/capybara/rack_test/browser.rb +14 -13
  14. data/lib/capybara/rack_test/form.rb +32 -27
  15. data/lib/capybara/result.rb +8 -11
  16. data/lib/capybara/rspec/compound.rb +16 -26
  17. data/lib/capybara/rspec/matcher_proxies.rb +28 -11
  18. data/lib/capybara/rspec/matchers.rb +67 -45
  19. data/lib/capybara/selector.rb +22 -25
  20. data/lib/capybara/selector/builders/css_builder.rb +3 -4
  21. data/lib/capybara/selector/builders/xpath_builder.rb +4 -6
  22. data/lib/capybara/selector/regexp_disassembler.rb +43 -44
  23. data/lib/capybara/selector/selector.rb +16 -11
  24. data/lib/capybara/selenium/driver.rb +28 -21
  25. data/lib/capybara/selenium/nodes/marionette_node.rb +6 -12
  26. data/lib/capybara/server.rb +1 -3
  27. data/lib/capybara/server/animation_disabler.rb +2 -2
  28. data/lib/capybara/session.rb +1 -1
  29. data/lib/capybara/spec/session/assert_all_of_selectors_spec.rb +29 -0
  30. data/lib/capybara/spec/session/click_button_spec.rb +6 -0
  31. data/lib/capybara/spec/session/click_link_spec.rb +1 -1
  32. data/lib/capybara/spec/session/has_all_selectors_spec.rb +1 -1
  33. data/lib/capybara/spec/session/has_any_selectors_spec.rb +25 -0
  34. data/lib/capybara/spec/session/html_spec.rb +7 -0
  35. data/lib/capybara/spec/session/node_spec.rb +5 -1
  36. data/lib/capybara/spec/session/refresh_spec.rb +4 -0
  37. data/lib/capybara/spec/session/window/switch_to_window_spec.rb +4 -0
  38. data/lib/capybara/spec/session/window/window_opened_by_spec.rb +4 -0
  39. data/lib/capybara/spec/session/window/window_spec.rb +4 -0
  40. data/lib/capybara/spec/session/window/windows_spec.rb +4 -0
  41. data/lib/capybara/spec/views/form.erb +2 -2
  42. data/lib/capybara/version.rb +1 -1
  43. data/spec/minitest_spec.rb +5 -1
  44. data/spec/minitest_spec_spec.rb +5 -1
  45. data/spec/regexp_dissassembler_spec.rb +5 -3
  46. data/spec/rspec_spec.rb +20 -0
  47. data/spec/selector_spec.rb +89 -3
  48. data/spec/selenium_spec_chrome.rb +3 -7
  49. data/spec/selenium_spec_marionette.rb +1 -4
  50. metadata +20 -6
  51. data/lib/capybara/xpath_patches.rb +0 -27
@@ -33,16 +33,15 @@ if defined?(::RSpec::Expectations::Version)
33
33
  end
34
34
  end
35
35
 
36
- class And < ::RSpec::Matchers::BuiltIn::Compound::And
37
- private
38
-
36
+ # @api private
37
+ module Synchronizer
39
38
  def match(_expected, actual)
40
39
  @evaluator = CapybaraEvaluator.new(actual)
41
40
  syncer = sync_element(actual)
42
41
  begin
43
42
  syncer.synchronize do
44
43
  @evaluator.reset
45
- raise ::Capybara::ElementNotFound unless [matcher_1_matches?, matcher_2_matches?].all?
44
+ raise ::Capybara::ElementNotFound unless synchronized_match?
46
45
 
47
46
  true
48
47
  end
@@ -62,32 +61,23 @@ if defined?(::RSpec::Expectations::Version)
62
61
  end
63
62
  end
64
63
 
65
- class Or < ::RSpec::Matchers::BuiltIn::Compound::Or
66
- private
64
+ class And < ::RSpec::Matchers::BuiltIn::Compound::And
65
+ include Synchronizer
67
66
 
68
- def match(_expected, actual)
69
- @evaluator = CapybaraEvaluator.new(actual)
70
- syncer = sync_element(actual)
71
- begin
72
- syncer.synchronize do
73
- @evaluator.reset
74
- raise ::Capybara::ElementNotFound unless [matcher_1_matches?, matcher_2_matches?].any?
67
+ private
75
68
 
76
- true
77
- end
78
- rescue StandardError
79
- false
80
- end
69
+ def synchronized_match?
70
+ [matcher_1_matches?, matcher_2_matches?].all?
81
71
  end
72
+ end
82
73
 
83
- def sync_element(el)
84
- if el.respond_to? :synchronize
85
- el
86
- elsif el.respond_to? :current_scope
87
- el.current_scope
88
- else
89
- Capybara.string(el)
90
- end
74
+ class Or < ::RSpec::Matchers::BuiltIn::Compound::Or
75
+ include Synchronizer
76
+
77
+ private
78
+
79
+ def synchronized_match?
80
+ [matcher_1_matches?, matcher_2_matches?].any?
91
81
  end
92
82
  end
93
83
  end
@@ -18,13 +18,34 @@ module Capybara
18
18
  end
19
19
  end
20
20
  end
21
+ end
22
+
23
+ if RUBY_ENGINE == 'jruby'
24
+ module Capybara::DSL
25
+ class <<self
26
+ remove_method :included
27
+
28
+ def included(base)
29
+ warn 'including Capybara::DSL in the global scope is not recommended!' if base == Object
30
+ base.send(:include, ::Capybara::RSpecMatcherProxies) if defined?(::RSpec::Matchers) && base.include?(::RSpec::Matchers)
31
+ super
32
+ end
33
+ end
34
+ end
21
35
 
22
- module DSLRSpecProxyInstaller
36
+ if defined?(::RSpec::Matchers)
37
+ module ::RSpec::Matchers
38
+ def self.included(base)
39
+ base.send(:include, ::Capybara::RSpecMatcherProxies) if base.include?(::Capybara::DSL)
40
+ super
41
+ end
42
+ end
43
+ end
44
+ else
45
+ module Capybara::DSLRSpecProxyInstaller
23
46
  module ClassMethods
24
47
  def included(base)
25
- if defined?(::RSpec::Matchers)
26
- base.include(::Capybara::RSpecMatcherProxies) if base.include?(::RSpec::Matchers)
27
- end
48
+ base.include(::Capybara::RSpecMatcherProxies) if defined?(::RSpec::Matchers) && base.include?(::RSpec::Matchers)
28
49
  super
29
50
  end
30
51
  end
@@ -36,7 +57,7 @@ module Capybara
36
57
  end
37
58
  end
38
59
 
39
- module RSpecMatcherProxyInstaller
60
+ module Capybara::RSpecMatcherProxyInstaller
40
61
  module ClassMethods
41
62
  def included(base)
42
63
  base.include(::Capybara::RSpecMatcherProxies) if base.include?(::Capybara::DSL)
@@ -51,11 +72,7 @@ module Capybara
51
72
  end
52
73
  end
53
74
 
54
- DSL.prepend ::Capybara::DSLRSpecProxyInstaller
55
- end
75
+ Capybara::DSL.prepend ::Capybara::DSLRSpecProxyInstaller
56
76
 
57
- if defined?(::RSpec::Matchers)
58
- module ::RSpec::Matchers
59
- prepend ::Capybara::RSpecMatcherProxyInstaller
60
- end
77
+ ::RSpec::Matchers.prepend ::Capybara::RSpecMatcherProxyInstaller if defined?(::RSpec::Matchers)
61
78
  end
@@ -25,20 +25,6 @@ module Capybara
25
25
 
26
26
  private
27
27
 
28
- def wrap_matches?(actual)
29
- yield(wrap(actual))
30
- rescue Capybara::ExpectationNotMet => err
31
- @failure_message = err.message
32
- false
33
- end
34
-
35
- def wrap_does_not_match?(actual)
36
- yield(wrap(actual))
37
- rescue Capybara::ExpectationNotMet => err
38
- @failure_message_when_negated = err.message
39
- false
40
- end
41
-
42
28
  def session_query_args
43
29
  if @args.last.is_a? Hash
44
30
  @args.last[:session_options] = session_options
@@ -60,13 +46,29 @@ module Capybara
60
46
  end
61
47
  end
62
48
 
63
- class HaveSelector < Matcher
49
+ class WrappedElementMatcher < Matcher
64
50
  def matches?(actual)
65
- wrap_matches?(actual) { |el| el.assert_selector(*@args, &@filter_block) }
51
+ element_matches?(wrap(actual))
52
+ rescue Capybara::ExpectationNotMet => err
53
+ @failure_message = err.message
54
+ false
66
55
  end
67
56
 
68
57
  def does_not_match?(actual)
69
- wrap_does_not_match?(actual) { |el| el.assert_no_selector(*@args, &@filter_block) }
58
+ element_does_not_match?(wrap(actual))
59
+ rescue Capybara::ExpectationNotMet => err
60
+ @failure_message_when_negated = err.message
61
+ false
62
+ end
63
+ end
64
+
65
+ class HaveSelector < WrappedElementMatcher
66
+ def element_matches?(el)
67
+ el.assert_selector(*@args, &@filter_block)
68
+ end
69
+
70
+ def element_does_not_match?(el)
71
+ el.assert_no_selector(*@args, &@filter_block)
70
72
  end
71
73
 
72
74
  def description
@@ -78,9 +80,9 @@ module Capybara
78
80
  end
79
81
  end
80
82
 
81
- class HaveAllSelectors < Matcher
82
- def matches?(actual)
83
- wrap_matches?(actual) { |el| el.assert_all_of_selectors(*@args, &@filter_block) }
83
+ class HaveAllSelectors < WrappedElementMatcher
84
+ def element_matches?(el)
85
+ el.assert_all_of_selectors(*@args, &@filter_block)
84
86
  end
85
87
 
86
88
  def does_not_match?(_actual)
@@ -92,9 +94,9 @@ module Capybara
92
94
  end
93
95
  end
94
96
 
95
- class HaveNoSelectors < Matcher
96
- def matches?(actual)
97
- wrap_matches?(actual) { |el| el.assert_none_of_selectors(*@args, &@filter_block) }
97
+ class HaveNoSelectors < WrappedElementMatcher
98
+ def element_matches?(el)
99
+ el.assert_none_of_selectors(*@args, &@filter_block)
98
100
  end
99
101
 
100
102
  def does_not_match?(_actual)
@@ -106,13 +108,27 @@ module Capybara
106
108
  end
107
109
  end
108
110
 
111
+ class HaveAnySelectors < WrappedElementMatcher
112
+ def element_matches?(el)
113
+ el.assert_any_of_selectors(*@args, &@filter_block)
114
+ end
115
+
116
+ def does_not_match?(_actual)
117
+ el.assert_none_of_selectors(*@args, &@filter_block)
118
+ end
119
+
120
+ def description
121
+ 'have any selectors'
122
+ end
123
+ end
124
+
109
125
  class MatchSelector < HaveSelector
110
- def matches?(actual)
111
- wrap_matches?(actual) { |el| el.assert_matches_selector(*@args, &@filter_block) }
126
+ def element_matches?(el)
127
+ el.assert_matches_selector(*@args, &@filter_block)
112
128
  end
113
129
 
114
- def does_not_match?(actual)
115
- wrap_does_not_match?(actual) { |el| el.assert_not_matches_selector(*@args, &@filter_block) }
130
+ def element_does_not_match?(el)
131
+ el.assert_not_matches_selector(*@args, &@filter_block)
116
132
  end
117
133
 
118
134
  def description
@@ -124,13 +140,13 @@ module Capybara
124
140
  end
125
141
  end
126
142
 
127
- class HaveText < Matcher
128
- def matches?(actual)
129
- wrap_matches?(actual) { |el| el.assert_text(*@args) }
143
+ class HaveText < WrappedElementMatcher
144
+ def element_matches?(el)
145
+ el.assert_text(*@args)
130
146
  end
131
147
 
132
- def does_not_match?(actual)
133
- wrap_does_not_match?(actual) { |el| el.assert_no_text(*@args) }
148
+ def element_does_not_match?(el)
149
+ el.assert_no_text(*@args)
134
150
  end
135
151
 
136
152
  def description
@@ -148,13 +164,13 @@ module Capybara
148
164
  end
149
165
  end
150
166
 
151
- class HaveTitle < Matcher
152
- def matches?(actual)
153
- wrap_matches?(actual) { |el| el.assert_title(*@args) }
167
+ class HaveTitle < WrappedElementMatcher
168
+ def element_matches?(el)
169
+ el.assert_title(*@args)
154
170
  end
155
171
 
156
- def does_not_match?(actual)
157
- wrap_does_not_match?(actual) { |el| el.assert_no_title(*@args) }
172
+ def element_does_not_match?(el)
173
+ el.assert_no_title(*@args)
158
174
  end
159
175
 
160
176
  def description
@@ -168,13 +184,13 @@ module Capybara
168
184
  end
169
185
  end
170
186
 
171
- class HaveCurrentPath < Matcher
172
- def matches?(actual)
173
- wrap_matches?(actual) { |el| el.assert_current_path(*@args) }
187
+ class HaveCurrentPath < WrappedElementMatcher
188
+ def element_matches?(el)
189
+ el.assert_current_path(*@args)
174
190
  end
175
191
 
176
- def does_not_match?(actual)
177
- wrap_does_not_match?(actual) { |el| el.assert_no_current_path(*@args) }
192
+ def element_does_not_match?(el)
193
+ el.assert_no_current_path(*@args)
178
194
  end
179
195
 
180
196
  def description
@@ -217,9 +233,9 @@ module Capybara
217
233
  end
218
234
  end
219
235
 
220
- class HaveStyle < Matcher
221
- def matches?(actual)
222
- wrap_matches?(actual) { |el| el.assert_style(*@args) }
236
+ class HaveStyle < WrappedElementMatcher
237
+ def element_matches?(el)
238
+ el.assert_style(*@args)
223
239
  end
224
240
 
225
241
  def does_not_match?(_actual)
@@ -275,6 +291,12 @@ module Capybara
275
291
  HaveNoSelectors.new(*args, &optional_filter_block)
276
292
  end
277
293
 
294
+ # RSpec matcher for whether the element(s) matching any of a group of selectors exist
295
+ # See {Capybara::Node::Matcher#assert_any_of_selectors}
296
+ def have_any_of_selectors(*args, &optional_filter_block)
297
+ HaveAnySelectors.new(*args, &optional_filter_block)
298
+ end
299
+
278
300
  # RSpec matcher for whether the current element matches a given selector
279
301
  # See {Capybara::Node::Matchers#assert_matches_selector}
280
302
  def match_selector(*args, &optional_filter_block)
@@ -64,7 +64,7 @@ Capybara.add_selector(:field) do
64
64
 
65
65
  describe_expression_filters do |type: nil, **options|
66
66
  desc = +''
67
- (expression_filters.keys - [:type]).each { |ef| desc << " with #{ef} #{options[ef]}" if options.key?(ef) }
67
+ (expression_filters.keys & options.keys).each { |ef| desc << " with #{ef} #{options[ef]}" }
68
68
  desc << " of type #{type.inspect}" if type
69
69
  desc
70
70
  end
@@ -75,11 +75,10 @@ Capybara.add_selector(:field) do
75
75
  end
76
76
 
77
77
  Capybara.add_selector(:fieldset) do
78
- xpath(:legend) do |locator, legend: nil, **|
78
+ xpath do |locator, legend: nil, **|
79
79
  locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.child(:legend)[XPath.string.n.is(locator.to_s)]
80
- locator_matchers |= XPath.attr(test_id) == locator if test_id
81
- xpath = XPath.descendant(:fieldset)
82
- xpath = xpath[locator_matchers] unless locator.nil?
80
+ locator_matchers |= XPath.attr(test_id) == locator.to_s if test_id
81
+ xpath = XPath.descendant(:fieldset)[locator && locator_matchers]
83
82
  xpath = xpath[XPath.child(:legend)[XPath.string.n.is(legend)]] if legend
84
83
  xpath
85
84
  end
@@ -88,7 +87,7 @@ Capybara.add_selector(:fieldset) do
88
87
  end
89
88
 
90
89
  Capybara.add_selector(:link) do
91
- xpath(:title, :alt) do |locator, href: true, alt: nil, title: nil, **|
90
+ xpath do |locator, href: true, alt: nil, title: nil, **|
92
91
  xpath = XPath.descendant(:a)
93
92
  xpath = xpath[@href_conditions = builder.attribute_conditions(href: href)]
94
93
 
@@ -192,8 +191,8 @@ end
192
191
  Capybara.add_selector(:fillable_field) do
193
192
  label 'field'
194
193
 
195
- xpath(:allow_self) do |locator, **options|
196
- xpath = XPath.axis(options[:allow_self] ? :"descendant-or-self" : :descendant, :input, :textarea)[
194
+ xpath do |locator, allow_self: nil, **options|
195
+ xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input, :textarea)[
197
196
  !XPath.attr(:type).one_of('submit', 'image', 'radio', 'checkbox', 'hidden', 'file')
198
197
  ]
199
198
  locate_field(xpath, locator, options)
@@ -223,8 +222,8 @@ end
223
222
  Capybara.add_selector(:radio_button) do
224
223
  label 'radio button'
225
224
 
226
- xpath(:allow_self) do |locator, **options|
227
- xpath = XPath.axis(options[:allow_self] ? :"descendant-or-self" : :descendant, :input)[
225
+ xpath do |locator, allow_self: nil, **options|
226
+ xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
228
227
  XPath.attr(:type) == 'radio'
229
228
  ]
230
229
  locate_field(xpath, locator, options)
@@ -241,8 +240,8 @@ Capybara.add_selector(:radio_button) do
241
240
  end
242
241
 
243
242
  Capybara.add_selector(:checkbox) do
244
- xpath(:allow_self) do |locator, **options|
245
- xpath = XPath.axis(options[:allow_self] ? :"descendant-or-self" : :descendant, :input)[
243
+ xpath do |locator, allow_self: nil, **options|
244
+ xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
246
245
  XPath.attr(:type) == 'checkbox'
247
246
  ]
248
247
  locate_field(xpath, locator, options)
@@ -379,8 +378,8 @@ end
379
378
 
380
379
  Capybara.add_selector(:file_field) do
381
380
  label 'file field'
382
- xpath(:allow_self) do |locator, **options|
383
- xpath = XPath.axis(options[:allow_self] ? :"descendant-or-self" : :descendant, :input)[
381
+ xpath do |locator, allow_self: nil, **options|
382
+ xpath = XPath.axis(allow_self ? :"descendant-or-self" : :descendant, :input)[
384
383
  XPath.attr(:type) == 'file'
385
384
  ]
386
385
  locate_field(xpath, locator, options)
@@ -411,14 +410,13 @@ Capybara.add_selector(:label) do
411
410
  end
412
411
 
413
412
  node_filter(:for) do |node, field_or_value|
414
- if field_or_value.is_a? Capybara::Node::Element
415
- if node[:for]
416
- field_or_value[:id] == node[:for]
417
- else
418
- field_or_value.find_xpath('./ancestor::label[1]').include? node.base
419
- end
413
+ # Non element values were handled through the expression filter
414
+ next true unless field_or_value.is_a? Capybara::Node::Element
415
+
416
+ if (for_val = node[:for])
417
+ field_or_value[:id] == for_val
420
418
  else
421
- true # Non element values were handled through the expression filter
419
+ field_or_value.find_xpath('./ancestor::label[1]').include? node.base
422
420
  end
423
421
  end
424
422
 
@@ -431,7 +429,7 @@ Capybara.add_selector(:label) do
431
429
  end
432
430
 
433
431
  Capybara.add_selector(:table) do
434
- xpath(:caption) do |locator, caption: nil, **|
432
+ xpath do |locator, caption: nil, **|
435
433
  xpath = XPath.descendant(:table)
436
434
  unless locator.nil?
437
435
  locator_matchers = (XPath.attr(:id) == locator.to_s) | XPath.descendant(:caption).is(locator.to_s)
@@ -448,15 +446,14 @@ Capybara.add_selector(:table) do
448
446
  end
449
447
 
450
448
  Capybara.add_selector(:frame) do
451
- xpath(:name) do |locator, **options|
449
+ xpath do |locator, name: nil, **|
452
450
  xpath = XPath.descendant(:iframe).union(XPath.descendant(:frame))
453
451
  unless locator.nil?
454
452
  locator_matchers = (XPath.attr(:id) == locator.to_s) | (XPath.attr(:name) == locator.to_s)
455
453
  locator_matchers |= XPath.attr(test_id) == locator if test_id
456
454
  xpath = xpath[locator_matchers]
457
455
  end
458
- xpath = expression_filters.keys.inject(xpath) { |memo, ef| memo[find_by_attr(ef, options[ef])] }
459
- xpath
456
+ xpath[find_by_attr(:name, name)]
460
457
  end
461
458
 
462
459
  describe_expression_filters do |name: nil, **|
@@ -35,11 +35,10 @@ module Capybara
35
35
  when XPath::Expression
36
36
  raise ArgumentError, 'XPath expressions are not supported for the :class filter with CSS based selectors'
37
37
  when Regexp
38
- strs = Selector::RegexpDisassembler.new(classes).substrings
39
- strs.map { |str| "[class*='#{str}'#{' i' if classes.casefold?}]" }.join
38
+ attribute_conditions(class: classes)
40
39
  else
41
- cls = Array(classes).group_by { |cl| cl.start_with? '!' }
42
- (cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl)}" } +
40
+ cls = Array(classes).group_by { |cl| cl.start_with?('!') && !cl.start_with?('!!!') }
41
+ (cls[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl.sub(/^!!/, ''))}" } +
43
42
  cls[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1..-1))})" }).join
44
43
  end
45
44
  end