We prefer to run our end-to-end tests with headless Chrome. While it's a very stable solution overall, we sometimes see the headless Chrome process freeze (or the Capybara driver losing connection, we're not sure).
The effect is that your test suite suddenly stops progressing without an error. You will eventually see an error after a long timeout but until then it will seem that your suite is frozen. If you're also using capybara-screenshot Show archive.org snapshot you will sit out that timeout twice, as capybara-screen again tries to communicate with the dead Chrome process.
Workaround
The code below will address the issue in two ways:
- It will close the Chrome process after groups of 50 tests, making the entire issue very unlikely. Capybara will automatically start a new process for the next test.
- In case Chrome communication breaks down, it will immediately raise an error instead of waiting for a long timeout.
The code expands on work by Tobias Kraze Show archive.org snapshot .
Include the module
Include the following StabilizeHeadlessChrome
module in your tests:
class StabilizeHeadlessChrome
class Reaper
MAX_SCENARIOS_PER_DRIVER = 50
class << self
def driver_scenario_count
@driver_scenario_count || 0
end
attr_writer :driver_scenario_count
def browser_died?
!!@browser_died
end
attr_writer :browser_died
def scenario_started
self.browser_died = false
end
def scenario_ended
return unless session.driver.is_a?(::Capybara::Selenium::Driver)
self.driver_scenario_count += 1
if !browser_died? && driver_scenario_count >= MAX_SCENARIOS_PER_DRIVER
kill_session!
end
end
def browser_died!
self.browser_died = true
kill_session!
end
def kill_session!
self.driver_scenario_count = 0
# This closes the browser
session.quit
end
private
def session
::Capybara.current_session
end
end
end
class CustomHttpClient < Selenium::WebDriver::Remote::Http::Default
def initialize(open_timeout: 10, read_timeout: 60)
super
end
private
def response_for(request)
if Reaper.browser_died?
raise 'Waiting for session to restart'
else
super
end
rescue Net::OpenTimeout, Net::ReadTimeout
# Chrome just died on us. We will reset the session, which causes Chrome to restart.
# In this case, capturing a screenshot will not work for this scenario.
warn "\n\e[31mKilling Capybara session!\e[0m\n"
::Reaper.browser_died!
raise
end
end
end
Call the reaper
To restart the Chrome process after 50 tests, Now call StabilizeHeadlessChrome::Reaper
when your test starts and ends.
E.g. in Cucumber:
Before do
StabilizeHeadlessChrome::Reaper.scenario_started
end
After do
StabilizeHeadlessChrome::Reaper.scenario_ended
end
Adjust driver registration
To get early errors (instead of long timeouts) you need to pass a new :http_client
option when registering a new Capybara driver.
Your old driver registration looks like this:
Capybara.register_driver :selenium do |app|
...
Capybara::Selenium::Driver.new(app,
browser: :chrome,
options: options
)
end
It now needs to look like this:
Capybara.register_driver :selenium do |app|
...
Capybara::Selenium::Driver.new(app,
browser: :chrome,
options: options,
http_client: StabilizeHeadlessChrome::CustomHttpClient.new
)
end