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