capybara 3.30.0 → 3.33.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +72 -0
  3. data/README.md +10 -3
  4. data/lib/capybara.rb +17 -7
  5. data/lib/capybara/cucumber.rb +1 -1
  6. data/lib/capybara/dsl.rb +10 -2
  7. data/lib/capybara/minitest.rb +232 -144
  8. data/lib/capybara/minitest/spec.rb +153 -97
  9. data/lib/capybara/node/actions.rb +16 -20
  10. data/lib/capybara/node/element.rb +13 -8
  11. data/lib/capybara/node/finders.rb +5 -1
  12. data/lib/capybara/node/matchers.rb +28 -21
  13. data/lib/capybara/node/simple.rb +1 -1
  14. data/lib/capybara/queries/base_query.rb +2 -1
  15. data/lib/capybara/queries/selector_query.rb +8 -1
  16. data/lib/capybara/queries/style_query.rb +1 -1
  17. data/lib/capybara/queries/text_query.rb +6 -0
  18. data/lib/capybara/rack_test/browser.rb +3 -1
  19. data/lib/capybara/rack_test/node.rb +34 -9
  20. data/lib/capybara/registration_container.rb +44 -0
  21. data/lib/capybara/registrations/servers.rb +1 -1
  22. data/lib/capybara/result.rb +29 -5
  23. data/lib/capybara/rspec/matcher_proxies.rb +4 -4
  24. data/lib/capybara/rspec/matchers.rb +27 -27
  25. data/lib/capybara/rspec/matchers/base.rb +12 -6
  26. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  27. data/lib/capybara/rspec/matchers/have_ancestor.rb +4 -3
  28. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  29. data/lib/capybara/rspec/matchers/have_selector.rb +15 -7
  30. data/lib/capybara/rspec/matchers/have_sibling.rb +3 -3
  31. data/lib/capybara/rspec/matchers/have_text.rb +3 -3
  32. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  33. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  34. data/lib/capybara/rspec/matchers/match_style.rb +2 -2
  35. data/lib/capybara/rspec/matchers/spatial_sugar.rb +2 -1
  36. data/lib/capybara/selector.rb +12 -1
  37. data/lib/capybara/selector/definition.rb +5 -4
  38. data/lib/capybara/selector/definition/button.rb +1 -0
  39. data/lib/capybara/selector/definition/fillable_field.rb +1 -1
  40. data/lib/capybara/selector/definition/label.rb +1 -1
  41. data/lib/capybara/selector/definition/link.rb +8 -0
  42. data/lib/capybara/selector/definition/select.rb +31 -12
  43. data/lib/capybara/selector/definition/table.rb +1 -1
  44. data/lib/capybara/selector/selector.rb +4 -0
  45. data/lib/capybara/selenium/atoms/getAttribute.min.js +1 -1
  46. data/lib/capybara/selenium/atoms/src/getAttribute.js +1 -1
  47. data/lib/capybara/selenium/driver.rb +7 -4
  48. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +8 -10
  49. data/lib/capybara/selenium/driver_specializations/edge_driver.rb +7 -9
  50. data/lib/capybara/selenium/driver_specializations/firefox_driver.rb +2 -2
  51. data/lib/capybara/selenium/extensions/html5_drag.rb +24 -8
  52. data/lib/capybara/selenium/node.rb +92 -15
  53. data/lib/capybara/selenium/nodes/chrome_node.rb +4 -11
  54. data/lib/capybara/selenium/nodes/edge_node.rb +1 -1
  55. data/lib/capybara/selenium/nodes/firefox_node.rb +3 -3
  56. data/lib/capybara/selenium/patches/action_pauser.rb +26 -0
  57. data/lib/capybara/selenium/patches/logs.rb +3 -5
  58. data/lib/capybara/session.rb +33 -18
  59. data/lib/capybara/session/config.rb +3 -1
  60. data/lib/capybara/spec/public/test.js +58 -6
  61. data/lib/capybara/spec/session/all_spec.rb +45 -5
  62. data/lib/capybara/spec/session/assert_text_spec.rb +5 -5
  63. data/lib/capybara/spec/session/click_button_spec.rb +11 -0
  64. data/lib/capybara/spec/session/fill_in_spec.rb +29 -0
  65. data/lib/capybara/spec/session/find_spec.rb +11 -8
  66. data/lib/capybara/spec/session/has_button_spec.rb +16 -0
  67. data/lib/capybara/spec/session/has_css_spec.rb +12 -9
  68. data/lib/capybara/spec/session/has_current_path_spec.rb +2 -2
  69. data/lib/capybara/spec/session/has_field_spec.rb +16 -0
  70. data/lib/capybara/spec/session/has_select_spec.rb +32 -4
  71. data/lib/capybara/spec/session/has_selector_spec.rb +4 -4
  72. data/lib/capybara/spec/session/has_text_spec.rb +5 -1
  73. data/lib/capybara/spec/session/node_spec.rb +146 -30
  74. data/lib/capybara/spec/session/window/window_spec.rb +7 -7
  75. data/lib/capybara/spec/spec_helper.rb +2 -2
  76. data/lib/capybara/spec/test_app.rb +14 -18
  77. data/lib/capybara/spec/views/form.erb +13 -2
  78. data/lib/capybara/spec/views/with_dragula.erb +3 -1
  79. data/lib/capybara/spec/views/with_html.erb +2 -2
  80. data/lib/capybara/spec/views/with_js.erb +1 -0
  81. data/lib/capybara/version.rb +1 -1
  82. data/spec/capybara_spec.rb +1 -1
  83. data/spec/dsl_spec.rb +14 -1
  84. data/spec/minitest_spec.rb +1 -1
  85. data/spec/rack_test_spec.rb +13 -1
  86. data/spec/regexp_dissassembler_spec.rb +0 -4
  87. data/spec/result_spec.rb +40 -29
  88. data/spec/rspec/shared_spec_matchers.rb +65 -53
  89. data/spec/selector_spec.rb +1 -1
  90. data/spec/selenium_spec_chrome.rb +6 -3
  91. data/spec/selenium_spec_chrome_remote.rb +2 -0
  92. data/spec/server_spec.rb +41 -49
  93. data/spec/shared_selenium_node.rb +18 -0
  94. data/spec/shared_selenium_session.rb +25 -7
  95. data/spec/spec_helper.rb +1 -1
  96. metadata +5 -3
@@ -235,13 +235,16 @@ module Capybara
235
235
  # @option options [Integer] maximum Maximum number of matches that are expected to be found
236
236
  # @option options [Integer] minimum Minimum number of matches that are expected to be found
237
237
  # @option options [Range] between Number of matches found must be within the given range
238
+ # @option options [Boolean] allow_reload Beta feature - May be removed in any version.
239
+ # When `true` allows elements to be reloaded if they become stale. This is an advanced behavior and should only be used
240
+ # if you fully understand the potential ramifications. The results can be confusing on dynamic pages. Defaults to `false`
238
241
  # @overload all([kind = Capybara.default_selector], locator = nil, **options)
239
242
  # @overload all([kind = Capybara.default_selector], locator = nil, **options, &filter_block)
240
243
  # @yieldparam element [Capybara::Node::Element] The element being considered for inclusion in the results
241
244
  # @yieldreturn [Boolean] Should the element be considered in the results?
242
245
  # @return [Capybara::Result] A collection of found elements
243
246
  # @raise [Capybara::ExpectationNotMet] The number of elements found doesn't match the specified conditions
244
- def all(*args, **options, &optional_filter_block)
247
+ def all(*args, allow_reload: false, **options, &optional_filter_block)
245
248
  minimum_specified = options_include_minimum?(options)
246
249
  options = { minimum: 1 }.merge(options) unless minimum_specified
247
250
  options[:session_options] = session_options
@@ -250,6 +253,7 @@ module Capybara
250
253
  begin
251
254
  synchronize(query.wait) do
252
255
  result = query.resolve_for(self)
256
+ result.allow_reload! if allow_reload
253
257
  raise Capybara::ExpectationNotMet, result.failure_message unless result.matches_count?
254
258
 
255
259
  result
@@ -61,7 +61,7 @@ module Capybara
61
61
  # @return [Boolean] If the styles match
62
62
  #
63
63
  def matches_style?(styles, **options)
64
- make_predicate(options) { assert_matches_style(styles, options) }
64
+ make_predicate(options) { assert_matches_style(styles, **options) }
65
65
  end
66
66
 
67
67
  ##
@@ -123,8 +123,8 @@ module Capybara
123
123
  # @raise [Capybara::ExpectationNotMet] If the element doesn't have the specified styles
124
124
  #
125
125
  def assert_matches_style(styles, **options)
126
- query_args = _set_query_session_options(styles, **options)
127
- query = Capybara::Queries::StyleQuery.new(*query_args)
126
+ query_args, query_opts = _set_query_session_options(styles, options)
127
+ query = Capybara::Queries::StyleQuery.new(*query_args, **query_opts)
128
128
  synchronize(query.wait) do
129
129
  raise Capybara::ExpectationNotMet, query.failure_message unless query.resolves_for?(self)
130
130
  end
@@ -201,12 +201,10 @@ module Capybara
201
201
  selector = extract_selector(args)
202
202
  synchronize(wait) do
203
203
  res = args.map do |locator|
204
- begin
205
- assert_selector(selector, locator, options, &optional_filter_block)
206
- break nil
207
- rescue Capybara::ExpectationNotMet => e
208
- e.message
209
- end
204
+ assert_selector(selector, locator, options, &optional_filter_block)
205
+ break nil
206
+ rescue Capybara::ExpectationNotMet => e
207
+ e.message
210
208
  end
211
209
  raise Capybara::ExpectationNotMet, res.join(' or ') if res
212
210
 
@@ -672,8 +670,8 @@ module Capybara
672
670
  # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
673
671
  # @return [true]
674
672
  #
675
- def assert_text(*args)
676
- _verify_text(*args) do |count, query|
673
+ def assert_text(type_or_text, *args, **opts)
674
+ _verify_text(type_or_text, *args, **opts) do |count, query|
677
675
  unless query.matches_count?(count) && (count.positive? || query.expects_none?)
678
676
  raise Capybara::ExpectationNotMet, query.failure_message
679
677
  end
@@ -688,8 +686,8 @@ module Capybara
688
686
  # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
689
687
  # @return [true]
690
688
  #
691
- def assert_no_text(*args)
692
- _verify_text(*args) do |count, query|
689
+ def assert_no_text(type_or_text, *args, **opts)
690
+ _verify_text(type_or_text, *args, **opts) do |count, query|
693
691
  if query.matches_count?(count) && (count.positive? || query.expects_none?)
694
692
  raise Capybara::ExpectationNotMet, query.negative_failure_message
695
693
  end
@@ -711,7 +709,7 @@ module Capybara
711
709
  # @return [Boolean] Whether it exists
712
710
  #
713
711
  def has_text?(*args, **options)
714
- make_predicate(options) { assert_text(*args, options) }
712
+ make_predicate(options) { assert_text(*args, **options) }
715
713
  end
716
714
  alias_method :has_content?, :has_text?
717
715
 
@@ -723,7 +721,7 @@ module Capybara
723
721
  # @return [Boolean] Whether it doesn't exist
724
722
  #
725
723
  def has_no_text?(*args, **options)
726
- make_predicate(options) { assert_no_text(*args, options) }
724
+ make_predicate(options) { assert_no_text(*args, **options) }
727
725
  end
728
726
  alias_method :has_no_content?, :has_no_text?
729
727
 
@@ -832,8 +830,14 @@ module Capybara
832
830
  end
833
831
 
834
832
  def _verify_selector_result(query_args, optional_filter_block, query_type = Capybara::Queries::SelectorQuery)
835
- query_args = _set_query_session_options(*query_args)
836
- query = query_type.new(*query_args, &optional_filter_block)
833
+ # query_args, query_opts = if query_args[0].is_a? Symbol
834
+ # a,o = _set_query_session_options(*query_args.slice(2..))
835
+ # [query_args.slice(0..1).concat(a), o]
836
+ # else
837
+ # _set_query_session_options(*query_args)
838
+ # end
839
+ query_args, query_opts = _set_query_session_options(*query_args)
840
+ query = query_type.new(*query_args, **query_opts, &optional_filter_block)
837
841
  synchronize(query.wait) do
838
842
  yield query.resolve_for(self), query
839
843
  end
@@ -841,8 +845,8 @@ module Capybara
841
845
  end
842
846
 
843
847
  def _verify_match_result(query_args, optional_filter_block)
844
- query_args = _set_query_session_options(*query_args)
845
- query = Capybara::Queries::MatchQuery.new(*query_args, &optional_filter_block)
848
+ query_args, query_opts = _set_query_session_options(*query_args)
849
+ query = Capybara::Queries::MatchQuery.new(*query_args, **query_opts, &optional_filter_block)
846
850
  synchronize(query.wait) do
847
851
  yield query.resolve_for(parent || session&.document || query_scope)
848
852
  end
@@ -858,9 +862,12 @@ module Capybara
858
862
  true
859
863
  end
860
864
 
861
- def _set_query_session_options(*query_args, **query_options)
865
+ def _set_query_session_options(*query_args)
866
+ query_args, query_options = query_args.dup, {}
867
+ # query_options = query_args.pop if query_options.empty? && query_args.last.is_a?(Hash)
868
+ query_options = query_args.pop if query_args.last.is_a?(Hash)
862
869
  query_options[:session_options] = session_options
863
- query_args.push(query_options)
870
+ [query_args, query_options]
864
871
  end
865
872
 
866
873
  def make_predicate(options)
@@ -152,7 +152,7 @@ module Capybara
152
152
  yield # simple nodes don't need to wait
153
153
  end
154
154
 
155
- def allow_reload!
155
+ def allow_reload!(*)
156
156
  # no op
157
157
  end
158
158
 
@@ -79,7 +79,8 @@ module Capybara
79
79
  if count
80
80
  message << " #{occurrences count}"
81
81
  elsif between
82
- message << " between #{between.first} and #{between.end ? between.last : 'infinite'} times"
82
+ message << " between #{between.begin ? between.first : 1} and" \
83
+ " #{between.end ? between.last : 'infinite'} times"
83
84
  elsif maximum
84
85
  message << " at most #{occurrences maximum}"
85
86
  elsif minimum
@@ -6,6 +6,7 @@ module Capybara
6
6
  module Queries
7
7
  class SelectorQuery < Queries::BaseQuery
8
8
  attr_reader :expression, :selector, :locator, :options
9
+
9
10
  SPATIAL_KEYS = %i[above below left_of right_of near].freeze
10
11
  VALID_KEYS = SPATIAL_KEYS + COUNT_KEYS +
11
12
  %i[text id class style visible obscured exact exact_text normalize_ws match wait filter_set]
@@ -14,6 +15,7 @@ module Capybara
14
15
  def initialize(*args,
15
16
  session_options:,
16
17
  enable_aria_label: session_options.enable_aria_label,
18
+ enable_aria_role: session_options.enable_aria_role,
17
19
  test_id: session_options.test_id,
18
20
  selector_format: nil,
19
21
  order: nil,
@@ -30,7 +32,11 @@ module Capybara
30
32
 
31
33
  @selector = Selector.new(
32
34
  find_selector(args[0].is_a?(Symbol) ? args.shift : args[0]),
33
- config: { enable_aria_label: enable_aria_label, test_id: test_id },
35
+ config: {
36
+ enable_aria_label: enable_aria_label,
37
+ enable_aria_role: enable_aria_role,
38
+ test_id: test_id
39
+ },
34
40
  format: selector_format
35
41
  )
36
42
 
@@ -575,6 +581,7 @@ module Capybara
575
581
 
576
582
  class Rectangle
577
583
  attr_reader :top, :bottom, :left, :right
584
+
578
585
  def initialize(position)
579
586
  # rubocop:disable Style/RescueModifier
580
587
  @top = position['top'] rescue position['y']
@@ -34,7 +34,7 @@ module Capybara
34
34
  private
35
35
 
36
36
  def stringify_keys(hsh)
37
- hsh.each_with_object({}) { |(k, v), str_keys| str_keys[k.to_s] = v }
37
+ hsh.transform_keys(&:to_s)
38
38
  end
39
39
 
40
40
  def valid_keys
@@ -6,6 +6,8 @@ module Capybara
6
6
  class TextQuery < BaseQuery
7
7
  def initialize(type = nil, expected_text, session_options:, **options) # rubocop:disable Style/OptionalArguments
8
8
  @type = type.nil? ? default_type : type
9
+ raise ArgumentError, '${@type} is not a valid type for a text query' unless valid_types.include?(@type)
10
+
9
11
  @options = options
10
12
  super(@options)
11
13
  self.session_options = session_options
@@ -89,6 +91,10 @@ module Capybara
89
91
  COUNT_KEYS + %i[wait exact normalize_ws]
90
92
  end
91
93
 
94
+ def valid_types
95
+ %i[all visible]
96
+ end
97
+
92
98
  def check_visible_text?
93
99
  @type == :visible
94
100
  end
@@ -30,7 +30,9 @@ class Capybara::RackTest::Browser
30
30
 
31
31
  def submit(method, path, attributes)
32
32
  path = request_path if path.nil? || path.empty?
33
- process_and_follow_redirects(method, path, attributes, 'HTTP_REFERER' => current_url)
33
+ uri = build_uri(path)
34
+ uri.query = '' if method&.to_s&.downcase == 'get'
35
+ process_and_follow_redirects(method, uri.to_s, attributes, 'HTTP_REFERER' => current_url)
34
36
  end
35
37
 
36
38
  def follow(method, path, **attributes)
@@ -45,6 +45,7 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
45
45
 
46
46
  if radio? then set_radio(value)
47
47
  elsif checkbox? then set_checkbox(value)
48
+ elsif range? then set_range(value)
48
49
  elsif input_field? then set_input(value)
49
50
  elsif textarea? then native['_capybara_raw_value'] = value.to_s
50
51
  end
@@ -76,8 +77,8 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
76
77
  set(!checked?)
77
78
  elsif tag_name == 'label'
78
79
  click_label
79
- elsif tag_name == 'details'
80
- toggle_details
80
+ elsif (details = native.xpath('.//ancestor-or-self::details').last)
81
+ toggle_details(details)
81
82
  end
82
83
  end
83
84
 
@@ -123,9 +124,18 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
123
124
  alias_method "unchecked_#{meth_name}", meth_name
124
125
  private "unchecked_#{meth_name}" # rubocop:disable Style/AccessModifierDeclarations
125
126
 
126
- define_method meth_name do |*args|
127
- stale_check
128
- send("unchecked_#{meth_name}", *args)
127
+ if RUBY_VERSION >= '2.7'
128
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
129
+ def #{meth_name}(...)
130
+ stale_check
131
+ method(:"unchecked_#{meth_name}").call(...)
132
+ end
133
+ METHOD
134
+ else
135
+ define_method meth_name do |*args|
136
+ stale_check
137
+ send("unchecked_#{meth_name}", *args)
138
+ end
129
139
  end
130
140
  end
131
141
 
@@ -199,6 +209,14 @@ private
199
209
  end
200
210
  end
201
211
 
212
+ def set_range(value) # rubocop:disable Naming/AccessorMethodName
213
+ min, max, step = (native['min'] || 0).to_f, (native['max'] || 100).to_f, (native['step'] || 1).to_f
214
+ value = value.to_f
215
+ value = value.clamp(min, max)
216
+ value = ((value - min) / step).round * step + min
217
+ native['value'] = value.clamp(min, max)
218
+ end
219
+
202
220
  def set_input(value) # rubocop:disable Naming/AccessorMethodName
203
221
  if text_or_password? && attribute_is_not_blank?(:maxlength)
204
222
  # Browser behavior for maxlength="0" is inconsistent, so we stick with
@@ -238,11 +256,14 @@ private
238
256
  labelled_control.set(!labelled_control.checked?) if checkbox_or_radio?(labelled_control)
239
257
  end
240
258
 
241
- def toggle_details
242
- if native.has_attribute?('open')
243
- native.remove_attribute('open')
259
+ def toggle_details(details = nil)
260
+ details ||= native.xpath('.//ancestor-or-self::details').last
261
+ return unless details
262
+
263
+ if details.has_attribute?('open')
264
+ details.remove_attribute('open')
244
265
  else
245
- native.set_attribute('open', 'open')
266
+ details.set_attribute('open', 'open')
246
267
  end
247
268
  end
248
269
 
@@ -284,6 +305,10 @@ protected
284
305
  tag_name == 'textarea'
285
306
  end
286
307
 
308
+ def range?
309
+ input_field? && type == 'range'
310
+ end
311
+
287
312
  OPTION_OWNER_XPATH = XPath.parent(:optgroup, :select, :datalist).to_s.freeze
288
313
  DISABLED_BY_FIELDSET_XPATH = XPath.generate do |x|
289
314
  x.parent(:fieldset)[
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara
4
+ # @api private
5
+ class RegistrationContainer
6
+ def names
7
+ @registered.keys
8
+ end
9
+
10
+ def [](name)
11
+ @registered[name]
12
+ end
13
+
14
+ def []=(name, value)
15
+ warn 'DEPRECATED: Directly setting drivers/servers is deprecated, please use Capybara.register_driver/register_server instead'
16
+ @registered[name] = value
17
+ end
18
+
19
+ def method_missing(method_name, *args, **options, &block)
20
+ if @registered.respond_to?(method_name)
21
+ warn "DEPRECATED: Calling '#{method_name}' on the drivers/servers container is deprecated without replacement"
22
+ # RUBY 2.6 will send an empty hash rather than nothing with **options so fix that
23
+ return @registered.public_send(method_name, *args, &block) if options.empty?
24
+
25
+ return @registered.public_send(method_name, *args, **options, &block)
26
+ end
27
+ super
28
+ end
29
+
30
+ def respond_to_missing?(method_name, include_private = false)
31
+ @registered.respond_to?(method_name) || super
32
+ end
33
+
34
+ private
35
+
36
+ def initialize
37
+ @registered = {}
38
+ end
39
+
40
+ def register(name, block)
41
+ @registered[name] = block
42
+ end
43
+ end
44
+ end
@@ -7,7 +7,7 @@ end
7
7
  Capybara.register_server :webrick do |app, port, host, **options|
8
8
  require 'rack/handler/webrick'
9
9
  options = { Host: host, Port: port, AccessLog: [], Logger: WEBrick::Log.new(nil, 0) }.merge(options)
10
- Rack::Handler::WEBrick.run(app, options)
10
+ Rack::Handler::WEBrick.run(app, **options)
11
11
  end
12
12
 
13
13
  Capybara.register_server :puma do |app, port, host, **options|
@@ -31,6 +31,7 @@ module Capybara
31
31
  @filter_errors = []
32
32
  @results_enum = lazy_select_elements { |node| query.matches_filters?(node, @filter_errors) }
33
33
  @query = query
34
+ @allow_reload = false
34
35
  end
35
36
 
36
37
  def_delegators :full_results, :size, :length, :last, :values_at, :inspect, :sample
@@ -43,7 +44,7 @@ module Capybara
43
44
  @result_cache.each(&block)
44
45
  loop do
45
46
  next_result = @results_enum.next
46
- @result_cache << next_result
47
+ add_to_cache(next_result)
47
48
  yield next_result
48
49
  end
49
50
  self
@@ -59,7 +60,12 @@ module Capybara
59
60
  nil
60
61
  end
61
62
  when Range
62
- idx.end && idx.max # endless range will have end == nil
63
+ # idx.max is broken with beginless ranges
64
+ # idx.end && idx.max # endless range will have end == nil
65
+ max = idx.end
66
+ max = nil if max&.negative?
67
+ max -= 1 if max && idx.exclude_end?
68
+ max
63
69
  end
64
70
 
65
71
  if max_idx.nil?
@@ -94,7 +100,9 @@ module Capybara
94
100
  end
95
101
 
96
102
  if between
97
- min, max = between.min, (between.end && between.max)
103
+ min, max = (between.begin && between.min) || 1, between.end
104
+ max -= 1 if max && between.exclude_end?
105
+
98
106
  size = load_up_to(max ? max + 1 : min)
99
107
  return size <=> min unless between.include?(size)
100
108
  end
@@ -130,13 +138,26 @@ module Capybara
130
138
  @elements.length
131
139
  end
132
140
 
141
+ ##
142
+ # @api private
143
+ #
144
+ def allow_reload!
145
+ @allow_reload = true
146
+ self
147
+ end
148
+
133
149
  private
134
150
 
151
+ def add_to_cache(elem)
152
+ elem.allow_reload!(@result_cache.size) if @allow_reload
153
+ @result_cache << elem
154
+ end
155
+
135
156
  def load_up_to(num)
136
157
  loop do
137
158
  break if @result_cache.size >= num
138
159
 
139
- @result_cache << @results_enum.next
160
+ add_to_cache(@results_enum.next)
140
161
  end
141
162
  @result_cache.size
142
163
  end
@@ -150,10 +171,13 @@ module Capybara
150
171
  @rest ||= @elements - full_results
151
172
  end
152
173
 
153
- if (RUBY_PLATFORM == 'java') && (Gem::Version.new(JRUBY_VERSION) < Gem::Version.new('9.2.8.0'))
174
+ if RUBY_PLATFORM == 'java'
154
175
  # JRuby < 9.2.8.0 has an issue with lazy enumerators which
155
176
  # causes a concurrency issue with network requests here
156
177
  # https://github.com/jruby/jruby/issues/4212
178
+ # while JRuby >= 9.2.8.0 leaks threads when using lazy enumerators
179
+ # https://github.com/teamcapybara/capybara/issues/2349
180
+ # so disable the use and JRuby users will need to pay a performance penalty
157
181
  def lazy_select_elements(&block)
158
182
  @elements.select(&block).to_enum # non-lazy evaluation
159
183
  end