2929#++
3030
3131require "rack_session_access/capybara"
32+ require "retriable"
3233
3334module AuthenticationHelpers
3435 def self . included ( base )
@@ -47,7 +48,7 @@ def login_as(user)
4748 session_value_for ( user ) . to_s
4849 )
4950 else
50- page . set_rack_session ( session_value_for ( user ) )
51+ set_rack_session_with_retry ( session_value_for ( user ) )
5152 end
5253 end
5354
@@ -86,8 +87,32 @@ def logout
8687 allow ( RequestStore ) . to receive ( :[] ) . and_call_original
8788 end
8889
90+ # Chrome reports a node that went stale mid-command as an unhandled
91+ # inspector error, which selenium-webdriver surfaces as UnknownError
92+ # rather than StaleElementReferenceError, so Capybara's synchronize
93+ # does not retry it.
94+ STALE_PAGE_ERRORS = {
95+ Selenium ::WebDriver ::Error ::StaleElementReferenceError => nil ,
96+ Selenium ::WebDriver ::Error ::UnknownError => /Node with given id does not belong to the document/
97+ } . freeze
98+ private_constant :STALE_PAGE_ERRORS
99+
89100 private
90101
102+ # set_rack_session drives a real form on /rack_session: it submits the
103+ # form and immediately reads the page text to confirm the update. Under
104+ # Selenium that read can resolve the body node just before the submit's
105+ # navigation lands and then read it just after, hitting a stale node.
106+ # Writing the session data again is idempotent, so the whole call is
107+ # retried a bounded number of times. Retriable is called directly rather
108+ # than through retry_block because the latter no-ops under
109+ # RSPEC_RETRY_RETRY_COUNT=0, which is where this race bites most.
110+ def set_rack_session_with_retry ( session_value )
111+ Retriable . retriable ( tries : 3 , on : STALE_PAGE_ERRORS ) do
112+ page . set_rack_session ( session_value )
113+ end
114+ end
115+
91116 def js_enabled?
92117 RSpec . current_example . metadata [ :js ]
93118 end
0 commit comments