Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## [Unreleased](https://github.com/rubycdp/ferrum/compare/v0.18.0...main) ##

### Added
- `:ignore_js_errors` browser option, an `Array` of `Regexp` (empty by default) matched against the message of the
JavaScript errors `js_errors: true` would re-raise; a matching error is dropped instead
- `Ferrum::Frame#wait_for_selector` (also reachable from `Page`/`Browser`), which waits for an element to show up
in the DOM. It retries `at_css`/`at_xpath` every `interval` until a match is found or time runs out, returning `nil`
on timeout [#82]
Expand Down
8 changes: 8 additions & 0 deletions docs/2-customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ Ferrum::Browser.new(options)
* `:timeout` (Numeric) - The number of seconds we'll wait for a response when
communicating with browser. Default is 5.
* `:js_errors` (Boolean) - When true, JavaScript errors get re-raised in Ruby.
* `:ignore_js_errors` (Array<Regexp>) - JavaScript errors that are not re-raised
even when `:js_errors` is true, matched against the error message. Default is `[]`.
* `:pending_connection_errors` (Boolean) - Raise `PendingConnectionsError` when main frame is still waiting
for slow responses and timeout is reached. Default is false.
* `:browser_name` (Symbol) - `:chrome` by default, `:opera` and `:edge` are
Expand Down Expand Up @@ -130,6 +132,12 @@ Ferrum::Browser.new(url: "http://localhost:9222")
# Enable JavaScript error raising
Ferrum::Browser.new(js_errors: true)

# Raise JavaScript errors except for known browser and library noise
Ferrum::Browser.new(
js_errors: true,
ignore_js_errors: [/ResizeObserver loop completed with undelivered notifications\./, /^AbortError: /]
)

# Set custom timeout and slowmo for debugging
Ferrum::Browser.new(timeout: 10, slowmo: 0.5)

Expand Down
4 changes: 4 additions & 0 deletions lib/ferrum/browser.rb
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ class Browser
# @option options [Boolean] :js_errors
# When true, JavaScript errors get re-raised in Ruby.
#
# @option options [Array<Regexp>] :ignore_js_errors ([])
# JavaScript errors not re-raised even when `:js_errors` is true, matched
# against the error message.
#
# @option options [Boolean] :pending_connection_errors (true)
# When main frame is still waiting for slow responses while timeout is
# reached {PendingConnectionsError} is raised. It's better to figure out
Expand Down
35 changes: 34 additions & 1 deletion lib/ferrum/browser/options.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class Options
DEBUG_MODE = !ENV.fetch("FERRUM_DEBUG", nil).nil?

attr_reader :window_size, :logger, :ws_max_receive_size,
:js_errors, :base_url, :slowmo, :pending_connection_errors,
:js_errors, :ignore_js_errors, :base_url, :slowmo, :pending_connection_errors,
:url, :ws_url, :env, :process_timeout, :browser_name, :browser_path,
:save_path, :proxy, :port, :host, :headless, :incognito, :dockerize, :browser_options,
:ignore_default_browser_options, :xvfb, :flatten
Expand All @@ -34,6 +34,7 @@ def initialize(options = nil)
@protocol_timeout = @options.fetch(:protocol_timeout, DEFAULT_PROTOCOL_TIMEOUT)
@window_size = @options.fetch(:window_size, WINDOW_SIZE)
@js_errors = @options.fetch(:js_errors, false)
@ignore_js_errors = validate_ignore_js_errors(@options.fetch(:ignore_js_errors, []))
@headless = @options.fetch(:headless, true)
@incognito = @options.fetch(:incognito, true)
@dockerize = @options.fetch(:dockerize, false)
Expand Down Expand Up @@ -108,6 +109,38 @@ def validate_proxy(options)
options
end

#
# Validates the `:ignore_js_errors` option.
#
# @param [Array<Regexp>] patterns
# The `:ignore_js_errors` option as passed to {#initialize}.
#
# @return [Array<Regexp>]
# The same patterns, frozen.
#
# @raise [ArgumentError]
# If `patterns` is not an `Array`, or holds anything but `Regexp`.
#
def validate_ignore_js_errors(patterns)
unless patterns.is_a?(Array) && patterns.all?(Regexp)
raise ArgumentError, "ignore_js_errors must be an Array of Regexp"
end

patterns.dup.freeze
end

#
# Whether a JavaScript error message matches any `:ignore_js_errors` pattern.
#
# @param [String] message
# The error message, as {JavaScriptError#message} builds it.
#
# @return [Boolean]
#
def ignore_js_error?(message)
@ignore_js_errors.any? { |pattern| pattern.match?(message) }
end

#
# Raw options hash used to initialize the browser.
#
Expand Down
5 changes: 4 additions & 1 deletion lib/ferrum/page.rb
Original file line number Diff line number Diff line change
Expand Up @@ -534,8 +534,11 @@ def subscribe

if @options.js_errors
on("Runtime.exceptionThrown") do |params|
error = JavaScriptError.new(params["exceptionDetails"])
next if @options.ignore_js_error?(error.message)

# FIXME: https://jvns.ca/blog/2015/11/27/why-rubys-timeout-is-dangerous-and-thread-dot-raise-is-terrifying/
Thread.main.raise JavaScriptError, params["exceptionDetails"]
Thread.main.raise error
end
end

Expand Down
7 changes: 7 additions & 0 deletions sig/ferrum/browser/options.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ module Ferrum

attr_reader js_errors: bool

attr_reader ignore_js_errors: ::Array[Regexp]

attr_reader base_url: ::Addressable::URI?

attr_reader slowmo: ::Float
Expand Down Expand Up @@ -76,6 +78,7 @@ module Ferrum
@protocol_timeout: ::Numeric
@window_size: ::Array[::Integer]
@js_errors: bool
@ignore_js_errors: ::Array[Regexp]
@headless: bool
@incognito: bool
@flatten: bool
Expand Down Expand Up @@ -106,6 +109,10 @@ module Ferrum

def validate_proxy: (Hash[Symbol, (String | ::Integer)]? options) -> Hash[Symbol, (String | ::Integer)]?

def validate_ignore_js_errors: (::Array[Regexp] patterns) -> ::Array[Regexp]

def ignore_js_error?: (String message) -> bool

def to_h: () -> Hash[Symbol, untyped]

private
Expand Down
41 changes: 41 additions & 0 deletions spec/browser/options_spec.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,47 @@
# frozen_string_literal: true

describe Ferrum::Browser::Options do
describe "#ignore_js_errors" do
it "defaults to no patterns" do
expect(described_class.new.ignore_js_errors).to eq([])
end

it "keeps the given patterns" do
expect(described_class.new(ignore_js_errors: [/omg/, /zomg/]).ignore_js_errors).to eq([/omg/, /zomg/])
end

it "rejects a single pattern that is not wrapped in an Array" do
expect { described_class.new(ignore_js_errors: /omg/) }
.to raise_error(ArgumentError, "ignore_js_errors must be an Array of Regexp")
end

it "rejects nil" do
expect { described_class.new(ignore_js_errors: nil) }
.to raise_error(ArgumentError, "ignore_js_errors must be an Array of Regexp")
end

it "rejects a String pattern" do
expect { described_class.new(ignore_js_errors: ["omg"]) }
.to raise_error(ArgumentError, "ignore_js_errors must be an Array of Regexp")
end
end

describe "#ignore_js_error?" do
let(:options) { described_class.new(ignore_js_errors: [/Transition was skipped/, /^ReferenceError: /]) }

it "matches a message any pattern matches" do
expect(options.ignore_js_error?("ReferenceError: omg is not defined")).to be(true)
end

it "does not match a message no pattern covers" do
expect(options.ignore_js_error?("TypeError: Cannot read properties of undefined")).to be(false)
end

it "matches nothing by default" do
expect(described_class.new.ignore_js_error?("ReferenceError: omg is not defined")).to be(false)
end
end

describe "#protocol_timeout" do
it "defaults to a low value, sufficient for internal CDP bookkeeping" do
options = described_class.new
Expand Down
28 changes: 28 additions & 0 deletions spec/frame/runtime_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,34 @@
browser&.quit
end
end

context "with ignored javascript errors" do
let(:browser) do
Ferrum::Browser.new(base_url: base_url, js_errors: true,
ignore_js_errors: [/ResizeObserver loop/, /ReferenceError: (omg|foo)/])
end

after { browser.quit }

it "does not propagate an error whose message matches an ignored pattern" do
browser.execute "setTimeout(function() { omg }, 0)"
sleep 0.1
expect(browser.evaluate("1+1")).to eq(2)
end

it "does not propagate an ignored error thrown during page load" do
browser.go_to("/js_error")
expect(browser.body).to include("hello")
end

it "propagates an error that matches no ignored pattern" do
expect do
browser.execute "setTimeout(function() { bar }, 0)"
sleep 0.01
browser.execute ""
end.to raise_error(Ferrum::JavaScriptError, /ReferenceError.*bar/)
end
end
end

describe "#evaluate" do
Expand Down
Loading