capybara 3.6.0 → 3.7.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 (64) hide show
  1. checksums.yaml +4 -4
  2. data/History.md +16 -0
  3. data/README.md +5 -1
  4. data/lib/capybara.rb +2 -0
  5. data/lib/capybara/minitest/spec.rb +1 -1
  6. data/lib/capybara/node/actions.rb +34 -25
  7. data/lib/capybara/node/base.rb +15 -17
  8. data/lib/capybara/node/document_matchers.rb +1 -3
  9. data/lib/capybara/node/element.rb +11 -12
  10. data/lib/capybara/node/finders.rb +2 -1
  11. data/lib/capybara/node/simple.rb +13 -6
  12. data/lib/capybara/queries/base_query.rb +4 -4
  13. data/lib/capybara/queries/selector_query.rb +119 -94
  14. data/lib/capybara/queries/text_query.rb +2 -1
  15. data/lib/capybara/rack_test/form.rb +4 -4
  16. data/lib/capybara/rack_test/node.rb +5 -5
  17. data/lib/capybara/result.rb +23 -32
  18. data/lib/capybara/rspec/compound.rb +1 -1
  19. data/lib/capybara/rspec/matchers.rb +63 -61
  20. data/lib/capybara/selector.rb +28 -10
  21. data/lib/capybara/selector/css.rb +17 -17
  22. data/lib/capybara/selector/filter_set.rb +9 -9
  23. data/lib/capybara/selector/selector.rb +3 -4
  24. data/lib/capybara/selenium/driver.rb +73 -95
  25. data/lib/capybara/selenium/driver_specializations/chrome_driver.rb +4 -4
  26. data/lib/capybara/selenium/driver_specializations/marionette_driver.rb +9 -0
  27. data/lib/capybara/selenium/node.rb +127 -67
  28. data/lib/capybara/selenium/nodes/chrome_node.rb +3 -3
  29. data/lib/capybara/selenium/nodes/marionette_node.rb +14 -8
  30. data/lib/capybara/server.rb +2 -2
  31. data/lib/capybara/server/animation_disabler.rb +17 -3
  32. data/lib/capybara/server/middleware.rb +8 -4
  33. data/lib/capybara/session.rb +43 -37
  34. data/lib/capybara/session/config.rb +8 -6
  35. data/lib/capybara/spec/session/assert_text_spec.rb +14 -0
  36. data/lib/capybara/spec/session/attach_file_spec.rb +7 -0
  37. data/lib/capybara/spec/session/check_spec.rb +21 -0
  38. data/lib/capybara/spec/session/choose_spec.rb +15 -1
  39. data/lib/capybara/spec/session/fill_in_spec.rb +7 -0
  40. data/lib/capybara/spec/session/find_spec.rb +2 -1
  41. data/lib/capybara/spec/session/has_selector_spec.rb +18 -0
  42. data/lib/capybara/spec/session/has_text_spec.rb +14 -0
  43. data/lib/capybara/spec/session/node_spec.rb +2 -1
  44. data/lib/capybara/spec/session/reset_session_spec.rb +4 -4
  45. data/lib/capybara/spec/session/text_spec.rb +2 -1
  46. data/lib/capybara/spec/session/title_spec.rb +2 -1
  47. data/lib/capybara/spec/session/uncheck_spec.rb +8 -0
  48. data/lib/capybara/spec/session/within_spec.rb +2 -1
  49. data/lib/capybara/spec/spec_helper.rb +1 -32
  50. data/lib/capybara/spec/views/with_js.erb +3 -4
  51. data/lib/capybara/version.rb +1 -1
  52. data/spec/minitest_spec.rb +4 -0
  53. data/spec/minitest_spec_spec.rb +4 -0
  54. data/spec/rack_test_spec.rb +4 -4
  55. data/spec/rspec/shared_spec_matchers.rb +4 -2
  56. data/spec/selector_spec.rb +15 -1
  57. data/spec/selenium_spec_chrome.rb +1 -6
  58. data/spec/selenium_spec_chrome_remote.rb +1 -1
  59. data/spec/selenium_spec_firefox_remote.rb +2 -5
  60. data/spec/selenium_spec_ie.rb +41 -4
  61. data/spec/selenium_spec_marionette.rb +1 -25
  62. data/spec/shared_selenium_session.rb +74 -16
  63. data/spec/spec_helper.rb +41 -0
  64. metadata +2 -2
@@ -3,9 +3,8 @@
3
3
  module Capybara
4
4
  module Queries
5
5
  class SelectorQuery < Queries::BaseQuery
6
- attr_accessor :selector, :locator, :options, :expression, :find, :negative
7
-
8
- VALID_KEYS = COUNT_KEYS + %i[text id class visible exact exact_text match wait filter_set]
6
+ attr_reader :expression, :selector, :locator, :options
7
+ VALID_KEYS = COUNT_KEYS + %i[text id class visible exact exact_text normalize_ws match wait filter_set]
9
8
  VALID_MATCH = %i[first smart prefer_exact one].freeze
10
9
 
11
10
  def initialize(*args,
@@ -25,7 +24,7 @@ module Capybara
25
24
 
26
25
  raise ArgumentError, "Unused parameters passed to #{self.class.name} : #{args}" unless args.empty?
27
26
 
28
- @expression = @selector.call(@locator, @options.merge(selector_config: { enable_aria_label: enable_aria_label, test_id: test_id }))
27
+ @expression = selector.call(@locator, @options.merge(selector_config: { enable_aria_label: enable_aria_label, test_id: test_id }))
29
28
 
30
29
  warn_exact_usage
31
30
 
@@ -36,22 +35,22 @@ module Capybara
36
35
  def label; selector.label || selector.name; end
37
36
 
38
37
  def description(applied = false)
39
- @description = +''
40
- if !applied || @applied_filters
41
- @description << 'visible ' if visible == :visible
42
- @description << 'non-visible ' if visible == :hidden
38
+ desc = +''
39
+ if !applied || applied_filters
40
+ desc << 'visible ' if visible == :visible
41
+ desc << 'non-visible ' if visible == :hidden
43
42
  end
44
- @description << "#{label} #{locator.inspect}"
45
- if !applied || @applied_filters
46
- @description << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
47
- @description << " with exact text #{exact_text}" if exact_text.is_a?(String)
43
+ desc << "#{label} #{locator.inspect}"
44
+ if !applied || applied_filters
45
+ desc << " with#{' exact' if exact_text == true} text #{options[:text].inspect}" if options[:text]
46
+ desc << " with exact text #{exact_text}" if exact_text.is_a?(String)
48
47
  end
49
- @description << " with id #{options[:id]}" if options[:id]
50
- @description << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
51
- @description << selector.description(node_filters: !applied || (@applied_filters == :node), **options)
52
- @description << ' that also matches the custom filter block' if @filter_block && (!applied || (@applied_filters == :node))
53
- @description << " within #{@resolved_node.inspect}" if describe_within?
54
- @description
48
+ desc << " with id #{options[:id]}" if options[:id]
49
+ desc << " with classes [#{Array(options[:class]).join(',')}]" if options[:class]
50
+ desc << selector.description(node_filters: !applied || (applied_filters == :node), **options)
51
+ desc << ' that also matches the custom filter block' if @filter_block && (!applied || (applied_filters == :node))
52
+ desc << " within #{@resolved_node.inspect}" if describe_within?
53
+ desc
55
54
  end
56
55
 
57
56
  def applied_description
@@ -59,15 +58,9 @@ module Capybara
59
58
  end
60
59
 
61
60
  def matches_filters?(node)
61
+ return true if (@resolved_node&.== node) && options[:allow_self]
62
62
  @applied_filters ||= :system
63
- return false if options[:text] && !matches_text_filter(node, options[:text])
64
- return false if exact_text.is_a?(String) && !matches_exact_text_filter(node, exact_text)
65
-
66
- case visible
67
- when :visible then return false unless node.visible?
68
- when :hidden then return false if node.visible?
69
- end
70
-
63
+ return false unless matches_text_filter?(node) && matches_exact_text_filter?(node) && matches_visible_filter?(node)
71
64
  @applied_filters = :node
72
65
  matches_node_filters?(node) && matches_filter_block?(node)
73
66
  rescue *(node.respond_to?(:session) ? node.session.driver.invalid_element_errors : [])
@@ -106,17 +99,7 @@ module Capybara
106
99
  @applied_filters = false
107
100
  @resolved_node = node
108
101
  node.synchronize do
109
- children = if selector.format == :css
110
- node.find_css(css)
111
- else
112
- node.find_xpath(xpath(exact))
113
- end.map do |child|
114
- if node.is_a?(Capybara::Node::Base)
115
- Capybara::Node::Element.new(node.session, child, node, self)
116
- else
117
- Capybara::Node::Simple.new(child)
118
- end
119
- end
102
+ children = find_nodes_by_selector_format(node, exact).map(&method(:to_element))
120
103
  Capybara::Result.new(children, self)
121
104
  end
122
105
  end
@@ -136,15 +119,35 @@ module Capybara
136
119
 
137
120
  private
138
121
 
122
+ def applied_filters
123
+ @applied_filters ||= false
124
+ end
125
+
139
126
  def find_selector(locator)
140
127
  selector = if locator.is_a?(Symbol)
141
128
  Selector.all.fetch(locator) { |sel_type| raise ArgumentError, "Unknown selector type (:#{sel_type})" }
142
129
  else
143
- Selector.all.values.find { |s| s.match?(locator) }
130
+ Selector.all.values.find { |sel| sel.match?(locator) }
144
131
  end
145
132
  selector || Selector.all[session_options.default_selector]
146
133
  end
147
134
 
135
+ def find_nodes_by_selector_format(node, exact)
136
+ if selector.format == :css
137
+ node.find_css(css)
138
+ else
139
+ node.find_xpath(xpath(exact))
140
+ end
141
+ end
142
+
143
+ def to_element(node)
144
+ if @resolved_node.is_a?(Capybara::Node::Base)
145
+ Capybara::Node::Element.new(@resolved_node.session, node, @resolved_node, self)
146
+ else
147
+ Capybara::Node::Simple.new(node)
148
+ end
149
+ end
150
+
148
151
  def valid_keys
149
152
  VALID_KEYS + custom_keys
150
153
  end
@@ -171,7 +174,6 @@ module Capybara
171
174
 
172
175
  def matches_filter_block?(node)
173
176
  return true unless @filter_block
174
-
175
177
  if node.respond_to?(:session)
176
178
  node.session.using_wait_time(0) { @filter_block.call(node) }
177
179
  else
@@ -201,87 +203,91 @@ module Capybara
201
203
  unless VALID_MATCH.include?(match)
202
204
  raise ArgumentError, "invalid option #{match.inspect} for :match, should be one of #{VALID_MATCH.map(&:inspect).join(', ')}"
203
205
  end
204
- unhandled_options = @options.keys - valid_keys
205
- unhandled_options -= @options.keys.select do |option_name|
206
- expression_filters.any? { |_nmae, ef| ef.handles_option? option_name } ||
206
+ unhandled_options = @options.keys.reject do |option_name|
207
+ valid_keys.include?(option_name) ||
208
+ expression_filters.any? { |_name, ef| ef.handles_option? option_name } ||
207
209
  node_filters.any? { |_name, nf| nf.handles_option? option_name }
208
210
  end
209
211
 
210
212
  return if unhandled_options.empty?
211
213
  invalid_names = unhandled_options.map(&:inspect).join(', ')
212
- valid_names = valid_keys.map(&:inspect).join(', ')
214
+ valid_names = (valid_keys - [:allow_self]).map(&:inspect).join(', ')
213
215
  raise ArgumentError, "invalid keys #{invalid_names}, should be one of #{valid_names}"
214
216
  end
215
217
 
216
218
  def filtered_xpath(expr)
217
- if options.key?(:id) && !custom_keys.include?(:id)
218
- expr = if options[:id].is_a? XPath::Expression
219
- "(#{expr})[#{XPath.attr(:id)[options[:id]]}]"
219
+ if use_default_id_filter?
220
+ id_xpath = if options[:id].is_a? XPath::Expression
221
+ XPath.attr(:id)[options[:id]]
220
222
  else
221
- "(#{expr})[#{XPath.attr(:id) == options[:id]}]"
223
+ XPath.attr(:id) == options[:id]
222
224
  end
225
+ expr = "(#{expr})[#{id_xpath}]"
223
226
  end
224
- if options.key?(:class) && !custom_keys.include?(:class)
225
- class_xpath = if options[:class].is_a?(XPath::Expression)
226
- XPath.attr(:class)[options[:class]]
227
- else
228
- Array(options[:class]).map do |klass|
229
- if klass.start_with?('!')
230
- !XPath.attr(:class).contains_word(klass.slice(1))
231
- else
232
- XPath.attr(:class).contains_word(klass)
233
- end
234
- end.reduce(:&)
235
- end
236
- expr = "(#{expr})[#{class_xpath}]"
237
- end
227
+ expr = "(#{expr})[#{xpath_from_classes}]" if use_default_class_filter?
238
228
  expr
239
229
  end
240
230
 
241
231
  def filtered_css(expr)
242
- process_id = options.key?(:id) && !custom_keys.include?(:id)
243
- process_class = options.key?(:class) && !custom_keys.include?(:class)
232
+ ::Capybara::Selector::CSS.split(expr).map do |sel|
233
+ sel += css_from_id if use_default_id_filter?
234
+ sel += css_from_classes if use_default_class_filter?
235
+ sel
236
+ end.join(', ')
237
+ end
244
238
 
245
- if process_id && options[:id].is_a?(XPath::Expression)
246
- raise ArgumentError, 'XPath expressions are not supported for the :id filter with CSS based selectors'
247
- end
248
- if process_class && options[:class].is_a?(XPath::Expression)
239
+ def use_default_id_filter?
240
+ options.key?(:id) && !custom_keys.include?(:id)
241
+ end
242
+
243
+ def use_default_class_filter?
244
+ options.key?(:class) && !custom_keys.include?(:class)
245
+ end
246
+
247
+ def css_from_classes
248
+ if options[:class].is_a?(XPath::Expression)
249
249
  raise ArgumentError, 'XPath expressions are not supported for the :class filter with CSS based selectors'
250
250
  end
251
251
 
252
- if process_id || process_class
253
- expr = ::Capybara::Selector::CSS.split(expr).map do |sel|
254
- sel += "##{::Capybara::Selector::CSS.escape(options[:id])}" if process_id
255
- sel += css_from_classes(Array(options[:class])) if process_class
256
- sel
257
- end.join(', ')
258
- end
252
+ classes = Array(options[:class]).group_by { |cl| cl.start_with? '!' }
253
+ (classes[false].to_a.map { |cl| ".#{Capybara::Selector::CSS.escape(cl)}" } +
254
+ classes[true].to_a.map { |cl| ":not(.#{Capybara::Selector::CSS.escape(cl.slice(1))})" }).join
255
+ end
259
256
 
260
- expr
257
+ def css_from_id
258
+ if options[:id].is_a?(XPath::Expression)
259
+ raise ArgumentError, 'XPath expressions are not supported for the :id filter with CSS based selectors'
260
+ end
261
+ "##{::Capybara::Selector::CSS.escape(options[:id])}"
261
262
  end
262
263
 
263
- def css_from_classes(classes)
264
- classes = classes.group_by { |c| c.start_with? '!' }
265
- (classes[false].to_a.map { |c| ".#{Capybara::Selector::CSS.escape(c)}" } +
266
- classes[true].to_a.map { |c| ":not(.#{Capybara::Selector::CSS.escape(c.slice(1))})" }).join
264
+ def xpath_from_classes
265
+ return XPath.attr(:class)[options[:class]] if options[:class].is_a?(XPath::Expression)
266
+
267
+ Array(options[:class]).map do |klass|
268
+ if klass.start_with?('!')
269
+ !XPath.attr(:class).contains_word(klass.slice(1))
270
+ else
271
+ XPath.attr(:class).contains_word(klass)
272
+ end
273
+ end.reduce(:&)
267
274
  end
268
275
 
269
- def apply_expression_filters(expr)
276
+ def apply_expression_filters(expression)
270
277
  unapplied_options = options.keys - valid_keys
271
- expression_filters.inject(expr) do |memo, (name, ef)|
278
+ expression_filters.inject(expression) do |expr, (name, ef)|
272
279
  if ef.matcher?
273
- unapplied_options.select { |option_name| ef.handles_option?(option_name) }.each do |option_name|
280
+ unapplied_options.select { |option_name| ef.handles_option?(option_name) }.inject(expr) do |memo, option_name|
274
281
  unapplied_options.delete(option_name)
275
- memo = ef.apply_filter(memo, option_name, options[option_name])
282
+ ef.apply_filter(memo, option_name, options[option_name])
276
283
  end
277
- memo
278
284
  elsif options.key?(name)
279
285
  unapplied_options.delete(name)
280
- ef.apply_filter(memo, name, options[name])
286
+ ef.apply_filter(expr, name, options[name])
281
287
  elsif ef.default?
282
- ef.apply_filter(memo, name, ef.default)
288
+ ef.apply_filter(expr, name, ef.default)
283
289
  else
284
- memo
290
+ expr
285
291
  end
286
292
  end
287
293
  end
@@ -307,21 +313,40 @@ module Capybara
307
313
  node.is_a?(::Capybara::Node::Simple) && node.path == '/'
308
314
  end
309
315
 
310
- def matches_text_filter(node, value)
311
- return matches_exact_text_filter(node, value) if exact_text == true
316
+ def matches_text_filter?(node)
317
+ value = options[:text]
318
+ return true unless value
319
+ return matches_text_exactly?(node, value) if exact_text == true
312
320
  regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
313
- matches_text_regexp(node, regexp)
321
+ matches_text_regexp?(node, regexp)
314
322
  end
315
323
 
316
- def matches_exact_text_filter(node, value)
324
+ def matches_exact_text_filter?(node)
325
+ return true unless exact_text.is_a?(String)
326
+ matches_text_exactly?(node, exact_text)
327
+ end
328
+
329
+ def matches_visible_filter?(node)
330
+ case visible
331
+ when :visible then node.visible?
332
+ when :hidden then !node.visible?
333
+ else true
334
+ end
335
+ end
336
+
337
+ def matches_text_exactly?(node, value)
317
338
  regexp = value.is_a?(Regexp) ? value : /\A#{Regexp.escape(value.to_s)}\z/
318
- matches_text_regexp(node, regexp)
339
+ matches_text_regexp?(node, regexp)
340
+ end
341
+
342
+ def normalize_ws
343
+ options.fetch(:normalize_ws, session_options.default_normalize_ws)
319
344
  end
320
345
 
321
- def matches_text_regexp(node, regexp)
346
+ def matches_text_regexp?(node, regexp)
322
347
  text_visible = visible
323
348
  text_visible = :all if text_visible == :hidden
324
- node.text(text_visible).match(regexp)
349
+ !!node.text(text_visible, normalize_ws: normalize_ws).match(regexp)
325
350
  end
326
351
  end
327
352
  end
@@ -90,7 +90,8 @@ module Capybara
90
90
  end
91
91
 
92
92
  def text(node: @node, query_type: @type)
93
- node.text(query_type, normalize_ws: options[:normalize_ws])
93
+ normalize_ws = options.fetch(:normalize_ws, session_options.default_normalize_ws)
94
+ node.text(query_type, normalize_ws: normalize_ws)
94
95
  end
95
96
 
96
97
  def default_type
@@ -22,10 +22,10 @@ class Capybara::RackTest::Form < Capybara::RackTest::Node
22
22
  params = make_params
23
23
 
24
24
  form_element_types = %i[input select textarea]
25
- form_elements_xpath = XPath.generate do |x|
26
- xpath = x.descendant(*form_element_types).where(!x.attr(:form))
27
- xpath += x.anywhere(*form_element_types).where(x.attr(:form) == native[:id]) if native[:id]
28
- xpath.where(!x.attr(:disabled))
25
+ form_elements_xpath = XPath.generate do |xp|
26
+ xpath = xp.descendant(*form_element_types).where(!xp.attr(:form))
27
+ xpath += xp.anywhere(*form_element_types).where(xp.attr(:form) == native[:id]) if native[:id]
28
+ xpath.where(!xp.attr(:disabled))
29
29
  end.to_s
30
30
 
31
31
  native.xpath(form_elements_xpath).map do |field|
@@ -105,11 +105,11 @@ class Capybara::RackTest::Node < Capybara::Driver::Node
105
105
  end
106
106
 
107
107
  def find_xpath(locator)
108
- native.xpath(locator).map { |n| self.class.new(driver, n) }
108
+ native.xpath(locator).map { |el| self.class.new(driver, el) }
109
109
  end
110
110
 
111
111
  def find_css(locator)
112
- native.css(locator, Capybara::RackTest::CSSHandlers.new).map { |n| self.class.new(driver, n) }
112
+ native.css(locator, Capybara::RackTest::CSSHandlers.new).map { |el| self.class.new(driver, el) }
113
113
  end
114
114
 
115
115
  def ==(other)
@@ -165,7 +165,7 @@ private
165
165
  end
166
166
 
167
167
  def set_radio(_value) # rubocop:disable Naming/AccessorMethodName
168
- other_radios_xpath = XPath.generate { |x| x.anywhere(:input)[x.attr(:name) == self[:name]] }.to_s
168
+ other_radios_xpath = XPath.generate { |xp| xp.anywhere(:input)[xp.attr(:name) == self[:name]] }.to_s
169
169
  driver.dom.xpath(other_radios_xpath).each { |node| node.remove_attribute('checked') }
170
170
  native['checked'] = 'checked'
171
171
  end
@@ -185,11 +185,11 @@ private
185
185
  value = value.to_s[0...self[:maxlength].to_i]
186
186
  end
187
187
  if value.is_a?(Array) # Assert multiple attribute is present
188
- value.each do |v|
188
+ value.each do |val|
189
189
  new_native = native.clone
190
190
  new_native.remove_attribute('value')
191
191
  native.add_next_sibling(new_native)
192
- new_native['value'] = v.to_s
192
+ new_native['value'] = val.to_s
193
193
  end
194
194
  native.remove
195
195
  else
@@ -62,10 +62,7 @@ module Capybara
62
62
  if max_idx.nil?
63
63
  full_results[*args]
64
64
  else
65
- loop do
66
- break if @result_cache.size > max_idx
67
- @result_cache << @results_enum.next
68
- end
65
+ load_up_to(max_idx + 1)
69
66
  @result_cache[*args]
70
67
  end
71
68
  end
@@ -77,40 +74,26 @@ module Capybara
77
74
 
78
75
  def compare_count
79
76
  # Only check filters for as many elements as necessary to determine result
80
- if @query.options[:count]
81
- count_opt = Integer(@query.options[:count])
82
- loop do
83
- break if @result_cache.size > count_opt
84
- @result_cache << @results_enum.next
85
- end
86
- return @result_cache.size <=> count_opt
77
+ if (count = @query.options[:count])
78
+ count = Integer(count)
79
+ return load_up_to(count + 1) <=> count
87
80
  end
88
81
 
89
- if @query.options[:minimum]
90
- min_opt = Integer(@query.options[:minimum])
91
- begin
92
- @result_cache << @results_enum.next while @result_cache.size < min_opt
93
- rescue StopIteration
94
- return -1
95
- end
82
+ if (min = @query.options[:minimum])
83
+ min = Integer(min)
84
+ return -1 if load_up_to(min) < min
96
85
  end
97
86
 
98
- if @query.options[:maximum]
99
- max_opt = Integer(@query.options[:maximum])
100
- loop do
101
- return 1 if @result_cache.size > max_opt
102
- @result_cache << @results_enum.next
103
- end
87
+ if (max = @query.options[:maximum])
88
+ max = Integer(max)
89
+ return 1 if load_up_to(max + 1) > max
104
90
  end
105
91
 
106
- if @query.options[:between]
107
- min, max = @query.options[:between].minmax
108
- loop do
109
- break if @result_cache.size > max
110
- @result_cache << @results_enum.next
111
- end
112
- return 0 if @query.options[:between].include? @result_cache.size
113
- return @result_cache.size <=> min
92
+ if (between = @query.options[:between])
93
+ min, max = between.minmax
94
+ size = load_up_to(max + 1)
95
+ return 0 if between.include? size
96
+ return size <=> min
114
97
  end
115
98
 
116
99
  0
@@ -144,6 +127,14 @@ module Capybara
144
127
 
145
128
  private
146
129
 
130
+ def load_up_to(num)
131
+ loop do
132
+ break if @result_cache.size >= num
133
+ @result_cache << @results_enum.next
134
+ end
135
+ @result_cache.size
136
+ end
137
+
147
138
  def full_results
148
139
  loop { @result_cache << @results_enum.next }
149
140
  @result_cache