capybara 3.30.0 → 3.31.0

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