Raising JavaScript errors in Ruby E2E tests (RSpec, Cucumber)

Updated . Posted . Visible to the public. Repeats.

A JavaScript error in an E2E test with Selenium will not cause your test to fail. This may cause you to miss errors in your frontend code.

Using the BrowserConsole helper below you can check your browser's error console from your E2E tests.

The following will raise BrowserConsole::ErrorsPresent if there is an error on the browser console:

BrowserConsole.assert_no_errors!

Ignoring errors

You can ignore errors by their exact message:

BrowserConsole.ignore('Browser is burning')

You can ignore errors with messages matching a regular expression:

BrowserConsole.ignore(/burning/i)

You may also provide a block that inspects an error object Show archive.org snapshot .
To ignore the error, return a truthy value:

BrowserConsole.ignore do |error|
  error.message.size < 100
end

Default ignore rules

By default BrowserConsole ignores:

  • Warnings (error.level === "WARNING")
  • Errors containing the phrase Failed to load resource

To delete these defaults, call BrowseConsole.ignore_nothing before your own ignore rules:

# Remove default rules
BrowserConsole.ignore_nothing

# Add your own rules
BrowserConsole.ignore(...)
BrowserConsole.ignore(...)

Automatic checking in RSpec feature specs

In your spec_helper.rb you may configure to automatically check for errors after every user interaction that goes through Capybara.
This requires capybara-lockstep Show archive.org snapshot 1.3+.

Capybara::Lockstep.after_synchronize do
  BrowserConsole.assert_no_errors!
end

Note that checking the browser console after every Capybara command will slow down your E2E tests somewhat. A compromise might be to only check the console when the spec has ended:

RSpec.configure do |config|
  config.after do
   BrowserConsole.assert_no_errors!
  end
end

Ignoring JavaScript errors for a spec

We can configure RSpec to ignore JavaScript errors in specs tagged as { mute_js_errors: true }:

RSpec.configure do |config|
  config.around(mute_js_errors: true) do |example|
    BrowserConsole.mute(&example)
  end
end

We can now tag individual scenarios to ignore all JavaScript errors:

scenario 'Eating errors', js: true, mute_js_errors: true do
  ...
end

Automatic checking in Cucumber scenarios

In your env.rb you may configure to automatically check for errors after every user interaction that goes through Capybara.
This requires capybara-lockstep Show archive.org snapshot 1.3+.

Capybara::Lockstep.after_synchronize do
  BrowserConsole.assert_no_errors!
end

Note that checking the browser console after every Capybara command will slow down your E2E tests somewhat. A compromise might be to only check the console when the spec has ended:

After('not @mute-js-errors') do
  BrowserConsole.assert_no_errors!
end

Ignoring JavaScript errors for a scenario

We can configure Cucumber to ignore JavaScript errors in specs tagged with @mute-js-errors:

Around('@mute-js-errors') do |_scenario, block|
  BrowserConsole.mute(&block)
end

To ignore JavaScript errors for an individual scenario, tag it with @mute-js-errors:

@javascript @mute-js-errors
Scenario: Eating errors
  ...

The BrowserConsole helper

class BrowserConsole
  class ErrorsPresent < StandardError; end

  class << self

    def all_errors
      if enabled?
        driver_logs_proc.call.get(:browser)
      else
        []
      end
    rescue Capybara::NotSupportedByDriverError
      []
    end

    def filtered_errors
      all_errors.select do |error|
        ignore_rules.none? do |rule|
          case rule
          when Proc
            rule.call(error)
          when Regexp
            error.message =~ rule
          when String
            error.message == rule
          end
        end
      end
    end

    def assert_no_errors!
      errors = filtered_errors

      if errors.present?
        message = (['There are JavaScript errors:'] + errors.map(&:message)).join("\n\n")
        raise ErrorsPresent, message
      end
    end

    def ignore(pattern=nil, &block)
      ignore_rules << (pattern || block)
    end

    def mute(&block)
      @muted = true
      if block
        begin
          block.call
        ensure
          unmute
        end
      end
    end

    def unmute
      @muted = false
    end

    def self.current
      @current ||= new
    end

    def ignore_nothing
      @ignore_rules = []
    end

    private

    def muted?
      @muted || false
    end

    def ignore_rules
      @ignore_rules ||= default_ignore_rules
    end

    def default_ignore_rules
      [
        /Failed to load resource/,
        ->(error) { error.level == 'WARNING' },
      ]
    end

    def page
      Capybara.current_session
    end

    def enabled?
      !muted? && driver_has_logs? && !alert_present?
    end

    def alert_present?
      # Chrome 54 and/or Chromedriver 2.24 introduced a breaking change on how
      # accessing browser logs work.
      #
      # Apparently, while an alert/confirm is open, Chrome will block any requests
      # to its `getLog` API. This causes Selenium to time out with a `Net::ReadTimeout` error
      page.driver.browser.switch_to.alert
      true
    rescue Capybara::NotSupportedByDriverError, Selenium::WebDriver::Error::NoSuchAlertError
      false
    end

    def driver_has_logs?
      !!driver_logs_proc
    end
    
    def driver_logs_proc
      browser = page.driver.browser
    
      if browser.respond_to?(:logs) # selenium-webdriver >= 4
        proc { browser.logs }
      elsif browser.respond_to?(:manage) && browser.manage.respond_to?(:logs) # selenium-webdriver < 4
        proc { browser.manage.logs }
      end  
    end

  end
end
Henning Koch
Last edit
Daniel Straßner
License
Source code in this card is licensed under the MIT License.
Posted by Henning Koch to makandra dev (2018-08-27 15:20)