capybara 3.10.1 → 3.11.0

Sign up to get free protection for your applications and to get access to all the features.
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|