capybara 3.6.0 → 3.7.0

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