From 288c8dfe7a0553e27313771c32ad47f5e31133d9 Mon Sep 17 00:00:00 2001
From: Thomas Walpole
Date: Fri, 5 Jul 2019 14:12:38 -0700
Subject: [PATCH] Optionally allow `all` results to be reloaded when stale
---
History.md | 2 ++
lib/capybara/node/element.rb | 17 +++++++-------
lib/capybara/node/finders.rb | 6 ++++-
lib/capybara/node/simple.rb | 2 +-
lib/capybara/result.rb | 18 +++++++++++++--
lib/capybara/spec/session/all_spec.rb | 33 +++++++++++++++++++++++++++
lib/capybara/spec/views/form.erb | 2 +-
7 files changed, 67 insertions(+), 13 deletions(-)
diff --git a/History.md b/History.md
index 469ed9a12..34303e571 100644
--- a/History.md
+++ b/History.md
@@ -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
diff --git a/lib/capybara/node/element.rb b/lib/capybara/node/element.rb
index f2f428470..4774d4ba8 100644
--- a/lib/capybara/node/element.rb
+++ b/lib/capybara/node/element.rb
@@ -27,9 +27,11 @@ def initialize(session, base, query_scope, query)
@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 @@ def flash
# @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
diff --git a/lib/capybara/node/finders.rb b/lib/capybara/node/finders.rb
index a30f30daf..6abdb25c1 100644
--- a/lib/capybara/node/finders.rb
+++ b/lib/capybara/node/finders.rb
@@ -235,13 +235,16 @@ def find_by_id(id, **options, &optional_filter_block)
# @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 @@ def all(*args, **options, &optional_filter_block)
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
diff --git a/lib/capybara/node/simple.rb b/lib/capybara/node/simple.rb
index 07e4806d9..c27511280 100644
--- a/lib/capybara/node/simple.rb
+++ b/lib/capybara/node/simple.rb
@@ -152,7 +152,7 @@ def synchronize(_seconds = nil)
yield # simple nodes don't need to wait
end
- def allow_reload!
+ def allow_reload!(*)
# no op
end
diff --git a/lib/capybara/result.rb b/lib/capybara/result.rb
index 33f5754fb..1bd3aa40f 100644
--- a/lib/capybara/result.rb
+++ b/lib/capybara/result.rb
@@ -31,6 +31,7 @@ def initialize(elements, query)
@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 @@ def each(&block)
@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 @@ def unfiltered_size
@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
diff --git a/lib/capybara/spec/session/all_spec.rb b/lib/capybara/spec/session/all_spec.rb
index f26dc2bd9..621e5ed90 100644
--- a/lib/capybara/spec/session/all_spec.rb
+++ b/lib/capybara/spec/session/all_spec.rb
@@ -47,6 +47,39 @@
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')
diff --git a/lib/capybara/spec/views/form.erb b/lib/capybara/spec/views/form.erb
index 6a2860eec..28c96ab7d 100644
--- a/lib/capybara/spec/views/form.erb
+++ b/lib/capybara/spec/views/form.erb
@@ -11,7 +11,7 @@
-