capybara 3.10.1 → 3.11.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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +13 -0
  3. data/README.md +2 -3
  4. data/lib/capybara.rb +16 -6
  5. data/lib/capybara/minitest.rb +8 -9
  6. data/lib/capybara/node/actions.rb +31 -28
  7. data/lib/capybara/node/base.rb +2 -1
  8. data/lib/capybara/node/document_matchers.rb +6 -2
  9. data/lib/capybara/node/element.rb +10 -10
  10. data/lib/capybara/node/finders.rb +13 -14
  11. data/lib/capybara/node/matchers.rb +1 -3
  12. data/lib/capybara/node/simple.rb +10 -2
  13. data/lib/capybara/queries/base_query.rb +7 -3
  14. data/lib/capybara/queries/selector_query.rb +60 -34
  15. data/lib/capybara/queries/style_query.rb +5 -1
  16. data/lib/capybara/queries/text_query.rb +2 -2
  17. data/lib/capybara/queries/title_query.rb +1 -1
  18. data/lib/capybara/rack_test/node.rb +16 -2
  19. data/lib/capybara/result.rb +9 -4
  20. data/lib/capybara/rspec/features.rb +4 -4
  21. data/lib/capybara/rspec/matcher_proxies.rb +3 -1
  22. data/lib/capybara/rspec/matchers.rb +25 -287
  23. data/lib/capybara/rspec/matchers/base.rb +98 -0
  24. data/lib/capybara/rspec/matchers/become_closed.rb +33 -0
  25. data/lib/capybara/rspec/matchers/compound.rb +88 -0
  26. data/lib/capybara/rspec/matchers/have_current_path.rb +29 -0
  27. data/lib/capybara/rspec/matchers/have_selector.rb +69 -0
  28. data/lib/capybara/rspec/matchers/have_style.rb +23 -0
  29. data/lib/capybara/rspec/matchers/have_text.rb +33 -0
  30. data/lib/capybara/rspec/matchers/have_title.rb +29 -0
  31. data/lib/capybara/rspec/matchers/match_selector.rb +27 -0
  32. data/lib/capybara/selector.rb +48 -20
  33. data/lib/capybara/selector/builders/xpath_builder.rb +3 -3
  34. data/lib/capybara/selector/css.rb +5 -5
  35. data/lib/capybara/selector/filters/base.rb +11 -3
  36. data/lib/capybara/selector/filters/expression_filter.rb +3 -3
  37. data/lib/capybara/selector/filters/node_filter.rb +16 -2
  38. data/lib/capybara/selector/regexp_disassembler.rb +116 -17
  39. data/lib/capybara/selector/selector.rb +52 -26
  40. data/lib/capybara/selenium/driver.rb +6 -2
  41. data/lib/capybara/selenium/node.rb +15 -14
  42. data/lib/capybara/selenium/nodes/marionette_node.rb +19 -5
  43. data/lib/capybara/selenium/patches/pause_duration_fix.rb +1 -3
  44. data/lib/capybara/server.rb +6 -1
  45. data/lib/capybara/server/animation_disabler.rb +1 -1
  46. data/lib/capybara/session.rb +4 -2
  47. data/lib/capybara/session/matchers.rb +7 -3
  48. data/lib/capybara/spec/public/test.js +5 -5
  49. data/lib/capybara/spec/session/all_spec.rb +5 -0
  50. data/lib/capybara/spec/session/has_css_spec.rb +4 -4
  51. data/lib/capybara/spec/session/has_field_spec.rb +17 -0
  52. data/lib/capybara/spec/session/node_spec.rb +45 -4
  53. data/lib/capybara/spec/spec_helper.rb +6 -1
  54. data/lib/capybara/spec/views/frame_child.erb +1 -1
  55. data/lib/capybara/spec/views/obscured.erb +44 -0
  56. data/lib/capybara/spec/views/with_html.erb +1 -1
  57. data/lib/capybara/version.rb +1 -1
  58. data/spec/rack_test_spec.rb +15 -0
  59. data/spec/regexp_dissassembler_spec.rb +88 -8
  60. data/spec/selector_spec.rb +3 -0
  61. data/spec/selenium_spec_chrome.rb +9 -15
  62. data/spec/selenium_spec_chrome_remote.rb +3 -2
  63. data/spec/selenium_spec_firefox_remote.rb +6 -2
  64. metadata +54 -3
  65. data/lib/capybara/rspec/compound.rb +0 -86
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/rspec/matchers/compound'
4
+
5
+ module Capybara
6
+ module RSpecMatchers
7
+ module Matchers
8
+ class Base
9
+ include ::Capybara::RSpecMatchers::Matchers::Compound if defined?(::Capybara::RSpecMatchers::Matchers::Compound)
10
+
11
+ attr_reader :failure_message, :failure_message_when_negated
12
+
13
+ def initialize(*args, &filter_block)
14
+ @args = args.dup
15
+ @filter_block = filter_block
16
+ end
17
+
18
+ private
19
+
20
+ def session_query_args
21
+ if @args.last.is_a? Hash
22
+ @args.last[:session_options] = session_options
23
+ else
24
+ @args.push(session_options: session_options)
25
+ end
26
+ @args
27
+ end
28
+
29
+ def session_options
30
+ @context_el ||= nil
31
+ if @context_el.respond_to? :session_options
32
+ @context_el.session_options
33
+ elsif @context_el.respond_to? :current_scope
34
+ @context_el.current_scope.session_options
35
+ else
36
+ Capybara.session_options
37
+ end
38
+ end
39
+ end
40
+
41
+ class WrappedElementMatcher < Base
42
+ def matches?(actual)
43
+ element_matches?(wrap(actual))
44
+ rescue Capybara::ExpectationNotMet => err
45
+ @failure_message = err.message
46
+ false
47
+ end
48
+
49
+ def does_not_match?(actual)
50
+ element_does_not_match?(wrap(actual))
51
+ rescue Capybara::ExpectationNotMet => err
52
+ @failure_message_when_negated = err.message
53
+ false
54
+ end
55
+
56
+ private
57
+
58
+ def wrap(actual)
59
+ actual = actual.to_capybara_node if actual.respond_to?(:to_capybara_node)
60
+ @context_el = if actual.respond_to?(:has_selector?)
61
+ actual
62
+ else
63
+ Capybara.string(actual.to_s)
64
+ end
65
+ end
66
+ end
67
+
68
+ class NegatedMatcher
69
+ include ::Capybara::RSpecMatchers::Matchers::Compound if defined?(::Capybara::RSpecMatchers::Matchers::Compound)
70
+
71
+ def initialize(matcher)
72
+ super()
73
+ @matcher = matcher
74
+ end
75
+
76
+ def matches?(actual)
77
+ @matcher.does_not_match?(actual)
78
+ end
79
+
80
+ def does_not_match?(actual)
81
+ @matcher.matches?(actual)
82
+ end
83
+
84
+ def description
85
+ "not #{@matcher.description}"
86
+ end
87
+
88
+ def failure_message
89
+ @matcher.failure_message_when_negated
90
+ end
91
+
92
+ def failure_message_when_negated
93
+ @matcher.failure_message
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ module RSpecMatchers
5
+ module Matchers
6
+ class BecomeClosed
7
+ def initialize(options)
8
+ @options = options
9
+ end
10
+
11
+ def matches?(window)
12
+ @window = window
13
+ @wait_time = Capybara::Queries::BaseQuery.wait(@options, window.session.config.default_max_wait_time)
14
+ timer = Capybara::Helpers.timer(expire_in: @wait_time)
15
+ while window.exists?
16
+ return false if timer.expired?
17
+
18
+ sleep 0.05
19
+ end
20
+ true
21
+ end
22
+
23
+ def failure_message
24
+ "expected #{@window.inspect} to become closed after #{@wait_time} seconds"
25
+ end
26
+
27
+ def failure_message_when_negated
28
+ "expected #{@window.inspect} not to become closed after #{@wait_time} seconds"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(::RSpec::Expectations::Version)
4
+ module Capybara
5
+ module RSpecMatchers
6
+ module Matchers
7
+ module Compound
8
+ include ::RSpec::Matchers::Composable
9
+
10
+ def and(matcher)
11
+ And.new(self, matcher)
12
+ end
13
+
14
+ def and_then(matcher)
15
+ ::RSpec::Matchers::BuiltIn::Compound::And.new(self, matcher)
16
+ end
17
+
18
+ def or(matcher)
19
+ Or.new(self, matcher)
20
+ end
21
+
22
+ class CapybaraEvaluator
23
+ def initialize(actual)
24
+ @actual = actual
25
+ @match_results = Hash.new { |hsh, matcher| hsh[matcher] = matcher.matches?(@actual) }
26
+ end
27
+
28
+ def matcher_matches?(matcher)
29
+ @match_results[matcher]
30
+ end
31
+
32
+ def reset
33
+ @match_results.clear
34
+ end
35
+ end
36
+
37
+ # @api private
38
+ module Synchronizer
39
+ def match(_expected, actual)
40
+ @evaluator = CapybaraEvaluator.new(actual)
41
+ syncer = sync_element(actual)
42
+ begin
43
+ syncer.synchronize do
44
+ @evaluator.reset
45
+ raise ::Capybara::ElementNotFound unless synchronized_match?
46
+
47
+ true
48
+ end
49
+ rescue StandardError
50
+ false
51
+ end
52
+ end
53
+
54
+ def sync_element(el)
55
+ if el.respond_to? :synchronize
56
+ el
57
+ elsif el.respond_to? :current_scope
58
+ el.current_scope
59
+ else
60
+ Capybara.string(el)
61
+ end
62
+ end
63
+ end
64
+
65
+ class And < ::RSpec::Matchers::BuiltIn::Compound::And
66
+ include Synchronizer
67
+
68
+ private
69
+
70
+ def synchronized_match?
71
+ [matcher_1_matches?, matcher_2_matches?].all?
72
+ end
73
+ end
74
+
75
+ class Or < ::RSpec::Matchers::BuiltIn::Compound::Or
76
+ include Synchronizer
77
+
78
+ private
79
+
80
+ def synchronized_match?
81
+ [matcher_1_matches?, matcher_2_matches?].any?
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/rspec/matchers/base'
4
+
5
+ module Capybara
6
+ module RSpecMatchers
7
+ module Matchers
8
+ class HaveCurrentPath < WrappedElementMatcher
9
+ def element_matches?(el)
10
+ el.assert_current_path(*@args)
11
+ end
12
+
13
+ def element_does_not_match?(el)
14
+ el.assert_no_current_path(*@args)
15
+ end
16
+
17
+ def description
18
+ "have current path #{current_path.inspect}"
19
+ end
20
+
21
+ private
22
+
23
+ def current_path
24
+ @args.first
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/rspec/matchers/base'
4
+
5
+ module Capybara
6
+ module RSpecMatchers
7
+ module Matchers
8
+ class HaveSelector < WrappedElementMatcher
9
+ def element_matches?(el)
10
+ el.assert_selector(*@args, &@filter_block)
11
+ end
12
+
13
+ def element_does_not_match?(el)
14
+ el.assert_no_selector(*@args, &@filter_block)
15
+ end
16
+
17
+ def description
18
+ "have #{query.description}"
19
+ end
20
+
21
+ def query
22
+ @query ||= Capybara::Queries::SelectorQuery.new(*session_query_args, &@filter_block)
23
+ end
24
+ end
25
+
26
+ class HaveAllSelectors < WrappedElementMatcher
27
+ def element_matches?(el)
28
+ el.assert_all_of_selectors(*@args, &@filter_block)
29
+ end
30
+
31
+ def does_not_match?(_actual)
32
+ raise ArgumentError, 'The have_all_selectors matcher does not support use with not_to/should_not'
33
+ end
34
+
35
+ def description
36
+ 'have all selectors'
37
+ end
38
+ end
39
+
40
+ class HaveNoSelectors < WrappedElementMatcher
41
+ def element_matches?(el)
42
+ el.assert_none_of_selectors(*@args, &@filter_block)
43
+ end
44
+
45
+ def does_not_match?(_actual)
46
+ raise ArgumentError, 'The have_none_of_selectors matcher does not support use with not_to/should_not'
47
+ end
48
+
49
+ def description
50
+ 'have no selectors'
51
+ end
52
+ end
53
+
54
+ class HaveAnySelectors < WrappedElementMatcher
55
+ def element_matches?(el)
56
+ el.assert_any_of_selectors(*@args, &@filter_block)
57
+ end
58
+
59
+ def does_not_match?(_actual)
60
+ el.assert_none_of_selectors(*@args, &@filter_block)
61
+ end
62
+
63
+ def description
64
+ 'have any selectors'
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/rspec/matchers/base'
4
+
5
+ module Capybara
6
+ module RSpecMatchers
7
+ module Matchers
8
+ class HaveStyle < WrappedElementMatcher
9
+ def element_matches?(el)
10
+ el.assert_style(*@args)
11
+ end
12
+
13
+ def does_not_match?(_actual)
14
+ raise ArgumentError, 'The have_style matcher does not support use with not_to/should_not'
15
+ end
16
+
17
+ def description
18
+ 'have style'
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/rspec/matchers/base'
4
+
5
+ module Capybara
6
+ module RSpecMatchers
7
+ module Matchers
8
+ class HaveText < WrappedElementMatcher
9
+ def element_matches?(el)
10
+ el.assert_text(*@args)
11
+ end
12
+
13
+ def element_does_not_match?(el)
14
+ el.assert_no_text(*@args)
15
+ end
16
+
17
+ def description
18
+ "text #{format(text)}"
19
+ end
20
+
21
+ def format(content)
22
+ content.inspect
23
+ end
24
+
25
+ private
26
+
27
+ def text
28
+ @args[0].is_a?(Symbol) ? @args[1] : @args[0]
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/rspec/matchers/base'
4
+
5
+ module Capybara
6
+ module RSpecMatchers
7
+ module Matchers
8
+ class HaveTitle < WrappedElementMatcher
9
+ def element_matches?(el)
10
+ el.assert_title(*@args)
11
+ end
12
+
13
+ def element_does_not_match?(el)
14
+ el.assert_no_title(*@args)
15
+ end
16
+
17
+ def description
18
+ "have title #{title.inspect}"
19
+ end
20
+
21
+ private
22
+
23
+ def title
24
+ @args.first
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'capybara/rspec/matchers/have_selector'
4
+
5
+ module Capybara
6
+ module RSpecMatchers
7
+ module Matchers
8
+ class MatchSelector < HaveSelector
9
+ def element_matches?(el)
10
+ el.assert_matches_selector(*@args, &@filter_block)
11
+ end
12
+
13
+ def element_does_not_match?(el)
14
+ el.assert_not_matches_selector(*@args, &@filter_block)
15
+ end
16
+
17
+ def description
18
+ "match #{query.description}"
19
+ end
20
+
21
+ def query
22
+ @query ||= Capybara::Queries::MatchQuery.new(*session_query_args, &@filter_block)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -34,7 +34,8 @@ Capybara.add_selector(:css) do
34
34
  end
35
35
 
36
36
  Capybara.add_selector(:id) do
37
- xpath { |id| XPath.descendant[XPath.attr(:id) == id.to_s] }
37
+ xpath { |id| XPath.descendant[builder.attribute_conditions(id: id)] }
38
+ locator_filter { |node, id| id.is_a?(Regexp) ? node[:id] =~ id : true }
38
39
  end
39
40
 
40
41
  Capybara.add_selector(:field) do
@@ -59,7 +60,10 @@ Capybara.add_selector(:field) do
59
60
 
60
61
  node_filter(:readonly, :boolean) { |node, value| !(value ^ node.readonly?) }
61
62
  node_filter(:with) do |node, with|
62
- with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
63
+ val = node.value
64
+ (with.is_a?(Regexp) ? val =~ with : val == with.to_s).tap do |res|
65
+ add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
66
+ end
63
67
  end
64
68
 
65
69
  describe_expression_filters do |type: nil, **options|
@@ -109,16 +113,13 @@ Capybara.add_selector(:link) do
109
113
 
110
114
  node_filter(:href) do |node, href|
111
115
  # If not a Regexp it's been handled in the main XPath
112
- href.is_a?(Regexp) ? node[:href].match(href) : true
116
+ (href.is_a?(Regexp) ? node[:href].match(href) : true).tap do |res|
117
+ add_error "Expected href to match #{href.inspect} but it was #{node[:href].inspect}" unless res
118
+ end
113
119
  end
114
120
 
115
121
  expression_filter(:download, valid_values: [true, false, String]) do |expr, download|
116
- mod = case download
117
- when true then XPath.attr(:download)
118
- when false then !XPath.attr(:download)
119
- when String then XPath.attr(:download) == download
120
- end
121
- expr[mod]
122
+ expr[builder.attribute_conditions(download: download)]
122
123
  end
123
124
 
124
125
  describe_expression_filters do |**options|
@@ -178,7 +179,9 @@ end
178
179
  Capybara.add_selector(:link_or_button) do
179
180
  label 'link or button'
180
181
  xpath do |locator, **options|
181
- self.class.all.values_at(:link, :button).map { |selector| selector.call(locator, **options, selector_config: @config) }.reduce(:union)
182
+ self.class.all.values_at(:link, :button).map do |selector|
183
+ instance_exec(locator, options, &selector.xpath)
184
+ end.reduce(:union)
182
185
  end
183
186
 
184
187
  node_filter(:disabled, :boolean, default: false, skip_if: :all) { |node, value| node.tag_name == 'a' || !(value ^ node.disabled?) }
@@ -210,7 +213,10 @@ Capybara.add_selector(:fillable_field) do
210
213
  filter_set(:_field, %i[disabled multiple name placeholder])
211
214
 
212
215
  node_filter(:with) do |node, with|
213
- with.is_a?(Regexp) ? node.value =~ with : node.value == with.to_s
216
+ val = node.value
217
+ (with.is_a?(Regexp) ? val =~ with : val == with.to_s).tap do |res|
218
+ add_error("Expected value to be #{with.inspect} but was #{val.inspect}") unless res
219
+ end
214
220
  end
215
221
 
216
222
  describe_expression_filters
@@ -231,7 +237,12 @@ Capybara.add_selector(:radio_button) do
231
237
 
232
238
  filter_set(:_field, %i[checked unchecked disabled name])
233
239
 
234
- node_filter(:option) { |node, value| node.value == value.to_s }
240
+ node_filter(:option) do |node, value|
241
+ val = node.value
242
+ (val == value.to_s).tap do |res|
243
+ add_error("Expected option value to be #{value.inspect} but it was #{val.inspect}") unless res
244
+ end
245
+ end
235
246
 
236
247
  describe_expression_filters
237
248
  describe_node_filters do |option: nil, **|
@@ -249,7 +260,12 @@ Capybara.add_selector(:checkbox) do
249
260
 
250
261
  filter_set(:_field, %i[checked unchecked disabled name])
251
262
 
252
- node_filter(:option) { |node, value| node.value == value.to_s }
263
+ node_filter(:option) do |node, value|
264
+ val = node.value
265
+ (val == value.to_s).tap do |res|
266
+ add_error("Expected option value to be #{value.inspect} but it was #{val.inspect}") unless res
267
+ end
268
+ end
253
269
 
254
270
  describe_expression_filters
255
271
  describe_node_filters do |option: nil, **|
@@ -273,23 +289,29 @@ Capybara.add_selector(:select) do
273
289
  else
274
290
  node.all(:xpath, './/option', visible: false, wait: false).map { |option| option.text(:all) }
275
291
  end
276
- options.sort == actual.sort
292
+ (options.sort == actual.sort).tap do |res|
293
+ add_error("Expected options #{options.inspect} found #{actual.inspect}") unless res
294
+ end
277
295
  end
278
296
 
279
297
  expression_filter(:with_options) do |expr, options|
280
298
  options.inject(expr) do |xpath, option|
281
- xpath[Capybara::Selector.all[:option].call(option)]
299
+ xpath[self.class.all[:option].call(option)]
282
300
  end
283
301
  end
284
302
 
285
303
  node_filter(:selected) do |node, selected|
286
304
  actual = node.all(:xpath, './/option', visible: false, wait: false).select(&:selected?).map { |option| option.text(:all) }
287
- Array(selected).sort == actual.sort
305
+ (Array(selected).sort == actual.sort).tap do |res|
306
+ add_error("Expected #{selected.inspect} to be selected found #{actual.inspect}") unless res
307
+ end
288
308
  end
289
309
 
290
310
  node_filter(:with_selected) do |node, selected|
291
311
  actual = node.all(:xpath, './/option', visible: false, wait: false).select(&:selected?).map { |option| option.text(:all) }
292
- (Array(selected) - actual).empty?
312
+ (Array(selected) - actual).empty?.tap do |res|
313
+ add_error("Expected at least #{selected.inspect} to be selected found #{actual.inspect}") unless res
314
+ end
293
315
  end
294
316
 
295
317
  describe_expression_filters do |with_options: nil, **opts|
@@ -320,12 +342,14 @@ Capybara.add_selector(:datalist_input) do
320
342
 
321
343
  node_filter(:options) do |node, options|
322
344
  actual = node.find("//datalist[@id=#{node[:list]}]", visible: :all).all(:datalist_option, wait: false).map(&:value)
323
- options.sort == actual.sort
345
+ (options.sort == actual.sort).tap do |res|
346
+ add_error("Expected #{options.inspect} options found #{actual.inspect}") unless res
347
+ end
324
348
  end
325
349
 
326
350
  expression_filter(:with_options) do |expr, options|
327
351
  options.inject(expr) do |xpath, option|
328
- xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[Capybara::Selector.all[:datalist_option].call(option)].attr(:id)]
352
+ xpath[XPath.attr(:list) == XPath.anywhere(:datalist)[self.class.all[:datalist_option].call(option)].attr(:id)]
329
353
  end
330
354
  end
331
355
 
@@ -471,7 +495,11 @@ Capybara.add_selector(:element) do
471
495
  end
472
496
 
473
497
  node_filter(:attributes, matcher: /.+/) do |node, name, val|
474
- val.is_a?(Regexp) ? node[name] =~ val : true
498
+ next true unless val.is_a?(Regexp)
499
+
500
+ (node[name] =~ val).tap do |res|
501
+ add_error("Expected #{name} to match #{val.inspect} but it was #{node[name]}") unless res
502
+ end
475
503
  end
476
504
 
477
505
  describe_expression_filters do |**options|