capybara 3.30.0 → 3.31.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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +21 -0
  3. data/README.md +1 -1
  4. data/lib/capybara/dsl.rb +10 -2
  5. data/lib/capybara/minitest.rb +18 -4
  6. data/lib/capybara/node/element.rb +11 -8
  7. data/lib/capybara/node/finders.rb +5 -1
  8. data/lib/capybara/node/matchers.rb +24 -15
  9. data/lib/capybara/node/simple.rb +1 -1
  10. data/lib/capybara/queries/base_query.rb +2 -1
  11. data/lib/capybara/rack_test/node.rb +34 -9
  12. data/lib/capybara/result.rb +24 -4
  13. data/lib/capybara/rspec/matchers.rb +27 -27
  14. data/lib/capybara/rspec/matchers/base.rb +12 -6
  15. data/lib/capybara/rspec/matchers/count_sugar.rb +2 -1
  16. data/lib/capybara/rspec/matchers/have_ancestor.rb +4 -3
  17. data/lib/capybara/rspec/matchers/have_current_path.rb +2 -2
  18. data/lib/capybara/rspec/matchers/have_selector.rb +15 -7
  19. data/lib/capybara/rspec/matchers/have_sibling.rb +3 -3
  20. data/lib/capybara/rspec/matchers/have_text.rb +2 -2
  21. data/lib/capybara/rspec/matchers/have_title.rb +2 -2
  22. data/lib/capybara/rspec/matchers/match_selector.rb +3 -3
  23. data/lib/capybara/rspec/matchers/match_style.rb +2 -2
  24. data/lib/capybara/rspec/matchers/spatial_sugar.rb +2 -1
  25. data/lib/capybara/selector.rb +2 -0
  26. data/lib/capybara/selector/definition/label.rb +1 -1
  27. data/lib/capybara/selector/definition/select.rb +31 -12
  28. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +1 -1
  29. data/lib/capybara/selenium/extensions/html5_drag.rb +24 -8
  30. data/lib/capybara/selenium/node.rb +23 -6
  31. data/lib/capybara/selenium/nodes/chrome_node.rb +4 -2
  32. data/lib/capybara/selenium/nodes/edge_node.rb +1 -1
  33. data/lib/capybara/selenium/nodes/firefox_node.rb +1 -1
  34. data/lib/capybara/session.rb +30 -15
  35. data/lib/capybara/spec/public/test.js +40 -6
  36. data/lib/capybara/spec/session/all_spec.rb +45 -5
  37. data/lib/capybara/spec/session/assert_text_spec.rb +5 -5
  38. data/lib/capybara/spec/session/fill_in_spec.rb +20 -0
  39. data/lib/capybara/spec/session/has_css_spec.rb +3 -3
  40. data/lib/capybara/spec/session/has_select_spec.rb +28 -0
  41. data/lib/capybara/spec/session/has_text_spec.rb +5 -1
  42. data/lib/capybara/spec/session/node_spec.rb +92 -3
  43. data/lib/capybara/spec/views/form.erb +6 -1
  44. data/lib/capybara/version.rb +1 -1
  45. data/spec/rack_test_spec.rb +0 -1
  46. data/spec/result_spec.rb +4 -0
  47. data/spec/selenium_spec_chrome.rb +2 -1
  48. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a441a0ffad497da4d5342b4a84c165d0922f92a049aa7035db212f210a1175c
4
- data.tar.gz: c24b692401fc4eb28d73bdee5dc8f20b01d6cca582b0d94a4e015ac35485bc63
3
+ metadata.gz: dd31d35629d475d7fdaba1d6e415599d995a08f27b28fa619a3a4449b61b91d8
4
+ data.tar.gz: 27c812629e2d99e8f4c62fa7961df9b499c5346b1c61092faf487f592356033d
5
5
  SHA512:
6
- metadata.gz: 3003c43d66f8cf415de12e9c9db95c5a60022aa10d63c3372074763049c7719041162eee9bb99fec17a7bf4190d252812b5d1513df3f22a586501f9f671dbea1
7
- data.tar.gz: de605e659b3c0e78065acd429e3d40c1e7758840b8cb8dd417229d440581b8363535d00a4193c51a2561a2ccde8b57b832d8176a8fb3b932a46e6682d161ddbd
6
+ metadata.gz: 946e3f2de0137ddaa6e3a2febba2c87244997737c54a10024133de110ca41b8aeddcb66e0437bef5d826e9fe21d4c42701674c3864bd578d24c30c7b1f90e2a9
7
+ data.tar.gz: f9409de3e2e0ea44e0bb6b6f77cce7e0e8f5a4d95c43fb62af2eb63d11e755d6a8c00b9b9b4d2bbe2729538d0d1038e92468e0a7994d1578b50e05b5227b87f4
data/History.md CHANGED
@@ -1,3 +1,24 @@
1
+ # Version 3.31.0
2
+ Release date: 2020-01-26
3
+
4
+ ### Added
5
+
6
+ * Support setting range inputs with the selenium driver [Andrew White]
7
+ * Support setting range inputs with the rack driver
8
+ * Support drop modifier keys in drag & drop [Elliot Crosby-McCullough]
9
+ * `enabled_options` and `disabled options` filters for select selector
10
+ * Support beginless ranges
11
+ * Optionally allow `all` results to be reloaded when stable - Beta feature - may be removed in
12
+ future version if problems occur
13
+
14
+ ### Fixed
15
+
16
+ * Fix Ruby 2.7 deprecation notices around keyword arguments. I have tried to do this without
17
+ any breaking changes, but due to the nature of the 2.7 changes and some selector types accepting
18
+ Hashes as locators there are a lot of edge cases. If you find any broken cases please report
19
+ them and I'll see if they're fixable.
20
+ * Clicking on details/summary element behavior in rack_test driver_
21
+
1
22
  # Version 3.30.0
2
23
  Release date: 2019-12-24
3
24
 
data/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/jnicklas/capybara?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
8
8
  [![SemVer](https://api.dependabot.com/badges/compatibility_score?dependency-name=capybara&package-manager=bundler&version-scheme=semver)](https://dependabot.com/compatibility-score.html?dependency-name=capybara&package-manager=bundler&version-scheme=semver)
9
9
 
10
- **Note** You are viewing the README for the 3.30.x Capybara release.
10
+ **Note** You are viewing the README for the 3.31.x stable branch of Capybara.
11
11
 
12
12
  Capybara helps you test web applications by simulating how a real user would
13
13
  interact with your app. It is agnostic about the driver running your tests and
@@ -47,8 +47,16 @@ module Capybara
47
47
  end
48
48
 
49
49
  Session::DSL_METHODS.each do |method|
50
- define_method method do |*args, &block|
51
- page.send method, *args, &block
50
+ if RUBY_VERSION >= '2.7'
51
+ class_eval <<~METHOD, __FILE__, __LINE__ + 1
52
+ def #{method}(...)
53
+ page.method("#{method}").call(...)
54
+ end
55
+ METHOD
56
+ else
57
+ define_method method do |*args, &block|
58
+ page.send method, *args, &block
59
+ end
52
60
  end
53
61
  end
54
62
  end
@@ -52,6 +52,7 @@ module Capybara
52
52
  raise ::Minitest::Assertion, e.message
53
53
  end
54
54
  ASSERTION
55
+ ruby2_keywords "assert_#{assertion_name}" if respond_to?(:ruby2_keywords)
55
56
  end
56
57
 
57
58
  alias_method :refute_title, :assert_no_title
@@ -109,6 +110,7 @@ module Capybara
109
110
  raise ::Minitest::Assertion, e.message
110
111
  end
111
112
  ASSERTION
113
+ ruby2_keywords "assert_#{assertion_name}" if respond_to?(:ruby2_keywords)
112
114
  end
113
115
 
114
116
  alias_method :refute_selector, :assert_no_selector
@@ -120,14 +122,16 @@ module Capybara
120
122
  define_method "assert_#{selector_type}" do |*args, &optional_filter_block|
121
123
  subject, args = determine_subject(args)
122
124
  locator, options = extract_locator(args)
123
- assert_selector(subject, selector_type.to_sym, locator, options, &optional_filter_block)
125
+ assert_selector(subject, selector_type.to_sym, locator, **options, &optional_filter_block)
124
126
  end
127
+ ruby2_keywords "assert_#{selector_type}" if respond_to?(:ruby2_keywords)
125
128
 
126
129
  define_method "assert_no_#{selector_type}" do |*args, &optional_filter_block|
127
130
  subject, args = determine_subject(args)
128
131
  locator, options = extract_locator(args)
129
- assert_no_selector(subject, selector_type.to_sym, locator, options, &optional_filter_block)
132
+ assert_no_selector(subject, selector_type.to_sym, locator, **options, &optional_filter_block)
130
133
  end
134
+ ruby2_keywords "assert_no_#{selector_type}" if respond_to?(:ruby2_keywords)
131
135
  alias_method "refute_#{selector_type}", "assert_no_#{selector_type}"
132
136
  end
133
137
 
@@ -135,14 +139,22 @@ module Capybara
135
139
  define_method "assert_#{field_type}_field" do |*args, &optional_filter_block|
136
140
  subject, args = determine_subject(args)
137
141
  locator, options = extract_locator(args)
138
- assert_selector(subject, :field, locator, options.merge(field_type.to_sym => true), &optional_filter_block)
142
+ assert_selector(subject, :field, locator, **options.merge(field_type.to_sym => true), &optional_filter_block)
139
143
  end
144
+ ruby2_keywords "assert_#{field_type}_field" if respond_to?(:ruby2_keywords)
140
145
 
141
146
  define_method "assert_no_#{field_type}_field" do |*args, &optional_filter_block|
142
147
  subject, args = determine_subject(args)
143
148
  locator, options = extract_locator(args)
144
- assert_no_selector(subject, :field, locator, options.merge(field_type.to_sym => true), &optional_filter_block)
149
+ assert_no_selector(
150
+ subject,
151
+ :field,
152
+ locator,
153
+ **options.merge(field_type.to_sym => true),
154
+ &optional_filter_block
155
+ )
145
156
  end
157
+ ruby2_keywords "assert_no_#{field_type}_field" if respond_to?(:ruby2_keywords)
146
158
  alias_method "refute_#{field_type}_field", "assert_no_#{field_type}_field"
147
159
  end
148
160
 
@@ -151,11 +163,13 @@ module Capybara
151
163
  subject, args = determine_subject(args)
152
164
  assert_matches_selector(subject, selector_type.to_sym, *args, &optional_filter_block)
153
165
  end
166
+ ruby2_keywords "assert_matches_#{selector_type}" if respond_to?(:ruby2_keywords)
154
167
 
155
168
  define_method "assert_not_matches_#{selector_type}" do |*args, &optional_filter_block|
156
169
  subject, args = determine_subject(args)
157
170
  assert_not_matches_selector(subject, selector_type.to_sym, *args, &optional_filter_block)
158
171
  end
172
+ ruby2_keywords "assert_not_matches_#{selector_type}" if respond_to?(:ruby2_keywords)
159
173
  alias_method "refute_matches_#{selector_type}", "assert_not_matches_#{selector_type}"
160
174
  end
161
175
 
@@ -27,9 +27,11 @@ module Capybara
27
27
  @query_scope = query_scope
28
28
  @query = query
29
29
  @allow_reload = false
30
+ @query_idx = nil
30
31
  end
31
32
 
32
- def allow_reload!
33
+ def allow_reload!(idx = nil)
34
+ @query_idx = idx
33
35
  @allow_reload = true
34
36
  end
35
37
 
@@ -407,6 +409,8 @@ module Capybara
407
409
  # @option options [Boolean] :html5 When using Chrome/Firefox with Selenium enables to force the use of HTML5
408
410
  # (true) or legacy (false) dragging. If not specified the driver will attempt to
409
411
  # detect the correct method to use.
412
+ # @option options [Array<Symbol>,Symbol] :drop_modifiers Modifier keys which should be held while the dragged element is dropped.
413
+ #
410
414
  #
411
415
  # @return [Capybara::Node::Element] The dragged element
412
416
  def drag_to(node, **options)
@@ -545,14 +549,13 @@ module Capybara
545
549
 
546
550
  # @api private
547
551
  def reload
548
- if @allow_reload
549
- begin
550
- reloaded = @query.resolve_for(query_scope.reload)&.first
552
+ return self unless @allow_reload
551
553
 
552
- @base = reloaded.base if reloaded
553
- rescue StandardError => e
554
- raise e unless catch_error?(e)
555
- end
554
+ begin
555
+ reloaded = @query.resolve_for(query_scope.reload)[@query_idx.to_i]
556
+ @base = reloaded.base if reloaded
557
+ rescue StandardError => e
558
+ raise e unless catch_error?(e)
556
559
  end
557
560
  self
558
561
  end
@@ -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
@@ -672,8 +672,8 @@ module Capybara
672
672
  # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
673
673
  # @return [true]
674
674
  #
675
- def assert_text(*args)
676
- _verify_text(*args) do |count, query|
675
+ def assert_text(type_or_text, *args, **opts)
676
+ _verify_text(type_or_text, *args, **opts) do |count, query|
677
677
  unless query.matches_count?(count) && (count.positive? || query.expects_none?)
678
678
  raise Capybara::ExpectationNotMet, query.failure_message
679
679
  end
@@ -688,8 +688,8 @@ module Capybara
688
688
  # @raise [Capybara::ExpectationNotMet] if the assertion hasn't succeeded during wait time
689
689
  # @return [true]
690
690
  #
691
- def assert_no_text(*args)
692
- _verify_text(*args) do |count, query|
691
+ def assert_no_text(type_or_text, *args, **opts)
692
+ _verify_text(type_or_text, *args, **opts) do |count, query|
693
693
  if query.matches_count?(count) && (count.positive? || query.expects_none?)
694
694
  raise Capybara::ExpectationNotMet, query.negative_failure_message
695
695
  end
@@ -711,7 +711,7 @@ module Capybara
711
711
  # @return [Boolean] Whether it exists
712
712
  #
713
713
  def has_text?(*args, **options)
714
- make_predicate(options) { assert_text(*args, options) }
714
+ make_predicate(options) { assert_text(*args, **options) }
715
715
  end
716
716
  alias_method :has_content?, :has_text?
717
717
 
@@ -723,7 +723,7 @@ module Capybara
723
723
  # @return [Boolean] Whether it doesn't exist
724
724
  #
725
725
  def has_no_text?(*args, **options)
726
- make_predicate(options) { assert_no_text(*args, options) }
726
+ make_predicate(options) { assert_no_text(*args, **options) }
727
727
  end
728
728
  alias_method :has_no_content?, :has_no_text?
729
729
 
@@ -832,8 +832,14 @@ module Capybara
832
832
  end
833
833
 
834
834
  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)
835
+ # query_args, query_opts = if query_args[0].is_a? Symbol
836
+ # a,o = _set_query_session_options(*query_args.slice(2..))
837
+ # [query_args.slice(0..1).concat(a), o]
838
+ # else
839
+ # _set_query_session_options(*query_args)
840
+ # end
841
+ query_args, query_opts = _set_query_session_options(*query_args)
842
+ query = query_type.new(*query_args, **query_opts, &optional_filter_block)
837
843
  synchronize(query.wait) do
838
844
  yield query.resolve_for(self), query
839
845
  end
@@ -841,8 +847,8 @@ module Capybara
841
847
  end
842
848
 
843
849
  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)
850
+ query_args, query_opts = _set_query_session_options(*query_args)
851
+ query = Capybara::Queries::MatchQuery.new(*query_args, **query_opts, &optional_filter_block)
846
852
  synchronize(query.wait) do
847
853
  yield query.resolve_for(parent || session&.document || query_scope)
848
854
  end
@@ -858,9 +864,12 @@ module Capybara
858
864
  true
859
865
  end
860
866
 
861
- def _set_query_session_options(*query_args, **query_options)
867
+ def _set_query_session_options(*query_args)
868
+ query_args, query_options = query_args.dup, {}
869
+ # query_options = query_args.pop if query_options.empty? && query_args.last.is_a?(Hash)
870
+ query_options = query_args.pop if query_args.last.is_a?(Hash)
862
871
  query_options[:session_options] = session_options
863
- query_args.push(query_options)
872
+ [query_args, query_options]
864
873
  end
865
874
 
866
875
  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
@@ -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)[
@@ -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,11 @@ 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 -= 1 if max && idx.exclude_end?
67
+ max
63
68
  end
64
69
 
65
70
  if max_idx.nil?
@@ -94,7 +99,9 @@ module Capybara
94
99
  end
95
100
 
96
101
  if between
97
- min, max = between.min, (between.end && between.max)
102
+ min, max = (between.begin && between.min) || 1, between.end
103
+ max -= 1 if max && between.exclude_end?
104
+
98
105
  size = load_up_to(max ? max + 1 : min)
99
106
  return size <=> min unless between.include?(size)
100
107
  end
@@ -130,13 +137,26 @@ module Capybara
130
137
  @elements.length
131
138
  end
132
139
 
140
+ ##
141
+ # @api private
142
+ #
143
+ def allow_reload!
144
+ @allow_reload = true
145
+ self
146
+ end
147
+
133
148
  private
134
149
 
150
+ def add_to_cache(elem)
151
+ elem.allow_reload!(@result_cache.size) if @allow_reload
152
+ @result_cache << elem
153
+ end
154
+
135
155
  def load_up_to(num)
136
156
  loop do
137
157
  break if @result_cache.size >= num
138
158
 
139
- @result_cache << @results_enum.next
159
+ add_to_cache(@results_enum.next)
140
160
  end
141
161
  @result_cache.size
142
162
  end