Optionally allow `all` results to be reloaded when stale

This commit is contained in:
Thomas Walpole 2019-07-05 14:12:38 -07:00
parent 0783f95d54
commit 288c8dfe7a
7 changed files with 67 additions and 13 deletions

View File

@ -9,6 +9,8 @@ Release date: unreleased
any breaking changes, but due to the nature of the 2.7 changes and some selector types accepting
Hashes as locators there are a lot of edge cases. If you find any broken cases please report
them and I'll see if they're fixable.
* Optionally allow `all` results to be reloaded when stable - Beta feature - may be removed in
future version
# Version 3.30.0
Release date: 2019-12-24

View File

@ -27,9 +27,11 @@ module Capybara
@query_scope = query_scope
@query = query
@allow_reload = false
@query_idx = nil
end
def allow_reload!
def allow_reload!(idx = nil)
@query_idx = idx
@allow_reload = true
end
@ -545,14 +547,13 @@ module Capybara
# @api private
def reload
if @allow_reload
begin
reloaded = @query.resolve_for(query_scope.reload)&.first
return self unless @allow_reload
@base = reloaded.base if reloaded
rescue StandardError => e
raise e unless catch_error?(e)
end
begin
reloaded = @query.resolve_for(query_scope.reload)[@query_idx.to_i]
@base = reloaded.base if reloaded
rescue StandardError => e
raise e unless catch_error?(e)
end
self
end

View File

@ -235,13 +235,16 @@ module Capybara
# @option options [Integer] maximum Maximum number of matches that are expected to be found
# @option options [Integer] minimum Minimum number of matches that are expected to be found
# @option options [Range] between Number of matches found must be within the given range
# @option options [Boolean] allow_reload Beta feature - May be removed in any version.
# When `true` allows elements to be reloaded if they become stale. This is an advanced behavior and should only be used
# if you fully understand the potential ramifications. The results can be confusing on dynamic pages. Defaults to `false`
# @overload all([kind = Capybara.default_selector], locator = nil, **options)
# @overload all([kind = Capybara.default_selector], locator = nil, **options, &filter_block)
# @yieldparam element [Capybara::Node::Element] The element being considered for inclusion in the results
# @yieldreturn [Boolean] Should the element be considered in the results?
# @return [Capybara::Result] A collection of found elements
# @raise [Capybara::ExpectationNotMet] The number of elements found doesn't match the specified conditions
def all(*args, **options, &optional_filter_block)
def all(*args, allow_reload: false, **options, &optional_filter_block)
minimum_specified = options_include_minimum?(options)
options = { minimum: 1 }.merge(options) unless minimum_specified
options[:session_options] = session_options
@ -250,6 +253,7 @@ module Capybara
begin
synchronize(query.wait) do
result = query.resolve_for(self)
result.allow_reload! if allow_reload
raise Capybara::ExpectationNotMet, result.failure_message unless result.matches_count?
result

View File

@ -152,7 +152,7 @@ module Capybara
yield # simple nodes don't need to wait
end
def allow_reload!
def allow_reload!(*)
# no op
end

View File

@ -31,6 +31,7 @@ module Capybara
@filter_errors = []
@results_enum = lazy_select_elements { |node| query.matches_filters?(node, @filter_errors) }
@query = query
@allow_reload = false
end
def_delegators :full_results, :size, :length, :last, :values_at, :inspect, :sample
@ -43,7 +44,7 @@ module Capybara
@result_cache.each(&block)
loop do
next_result = @results_enum.next
@result_cache << next_result
add_to_cache(next_result)
yield next_result
end
self
@ -130,13 +131,26 @@ module Capybara
@elements.length
end
##
# @api private
#
def allow_reload!
@allow_reload = true
self
end
private
def add_to_cache(elem)
elem.allow_reload!(@result_cache.size) if @allow_reload
@result_cache << elem
end
def load_up_to(num)
loop do
break if @result_cache.size >= num
@result_cache << @results_enum.next
add_to_cache(@results_enum.next)
end
@result_cache.size
end

View File

@ -47,6 +47,39 @@ Capybara::SpecHelper.spec '#all' do
expect { @session.all('//p', schmoo: 'foo') }.to raise_error(ArgumentError)
end
it 'should not reload by default', requires: [:driver] do
paras = @session.all(:css, 'p', minimum: 3)
expect { paras[0].text }.not_to raise_error
@session.refresh
expect { paras[0].text }.to raise_error do |err|
expect(err).to be_an_invalid_element_error(@session)
end
end
context 'with allow_reload' do
it 'should reload if true' do
paras = @session.all(:css, 'p', allow_reload: true, minimum: 3)
expect { paras[0].text }.not_to raise_error
@session.refresh
sleep 1 # Ensure page has started to reload
expect(paras[0]).to have_text('Lorem ipsum dolor')
expect(paras[1]).to have_text('Duis aute irure dolor')
end
it 'should not reload if false', requires: [:driver] do
paras = @session.all(:css, 'p', allow_reload: false, minimum: 3)
expect { paras[0].text }.not_to raise_error
@session.refresh
sleep 1 # Ensure page has started to reload
expect { paras[0].text }.to raise_error do |err|
expect(err).to be_an_invalid_element_error(@session)
end
expect { paras[2].text }.to raise_error do |err|
expect(err).to be_an_invalid_element_error(@session)
end
end
end
context 'with css selectors' do
it 'should find all elements using the given selector' do
expect(@session.all(:css, 'h1').first.text).to eq('This is a test')

View File

@ -11,7 +11,7 @@
<option>Miss</option>
<option disabled="disabled">Other</option>
</select>
</p>
</p>
<p>
<label for="customer_name">Customer Name