diff --git a/rb/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb b/rb/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb
index a9a57de8ea9d1..83b1909a78e2c 100644
--- a/rb/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb
+++ b/rb/lib/selenium/webdriver/common/driver_extensions/has_log_events.rb
@@ -21,29 +21,51 @@ module Selenium
module WebDriver
module DriverExtensions
module HasLogEvents
+ KINDS = %i[console exception].freeze
- #
#
# Registers listener to be called whenever browser receives
- # a new Console API message such as console.log().
+ # a new Console API message such as console.log() or an unhandled
+ # exception.
+ #
# This currently relies on DevTools so is only supported in
# Chromium browsers.
#
- # @example
+ # @example Collect console messages
# logs = []
- # driver.on_log_event do |event|
+ # driver.on_log_event(:console) do |event|
# logs.push(event)
# end
#
- # @param [#call] block which yields DevTools::ConsoleEvent
+ # @example Collect JavaScript exceptions
+ # exceptions = []
+ # driver.on_log_event(:exception) do |event|
+ # exceptions.push(event)
+ # end
#
+ # @param [Symbol] kind :console or :exception
+ # @param [#call] block which is called when event happens
+ # @yieldparam [DevTools::ConsoleEvent, DevTools::ExceptionEvent]
+ #
+
+ def on_log_event(kind, &block)
+ raise WebDriverError, "Don't know how to handle #{kind} events" unless KINDS.include?(kind)
- def on_log_event(&block)
- console_listeners_enabled = console_listeners.any?
- console_listeners << block
- return if console_listeners_enabled
+ enabled = log_listeners[kind].any?
+ log_listeners[kind] << block
+ return if enabled
devtools.runtime.enable
+ __send__("log_#{kind}_events")
+ end
+
+ private
+
+ def log_listeners
+ @log_listeners ||= Hash.new { |listeners, kind| listeners[kind] = [] }
+ end
+
+ def log_console_events
devtools.runtime.on(:console_api_called) do |params|
event = DevTools::ConsoleEvent.new(
type: params['type'],
@@ -51,16 +73,24 @@ def on_log_event(&block)
args: params['args']
)
- console_listeners.each do |listener|
+ log_listeners[:console].each do |listener|
listener.call(event)
end
end
end
- private
+ def log_exception_events
+ devtools.runtime.on(:exception_thrown) do |params|
+ event = DevTools::ExceptionEvent.new(
+ description: params.dig('exceptionDetails', 'exception', 'description'),
+ timestamp: params['timestamp'],
+ stacktrace: params.dig('exceptionDetails', 'stackTrace', 'callFrames')
+ )
- def console_listeners
- @console_listeners ||= []
+ log_listeners[:exception].each do |listener|
+ listener.call(event)
+ end
+ end
end
end # HasLogEvents
diff --git a/rb/lib/selenium/webdriver/devtools.rb b/rb/lib/selenium/webdriver/devtools.rb
index 7e02251d67763..6318a7dfebbe0 100644
--- a/rb/lib/selenium/webdriver/devtools.rb
+++ b/rb/lib/selenium/webdriver/devtools.rb
@@ -21,6 +21,7 @@ module Selenium
module WebDriver
class DevTools
autoload :ConsoleEvent, 'selenium/webdriver/devtools/console_event'
+ autoload :ExceptionEvent, 'selenium/webdriver/devtools/exception_event'
SUPPORTED_VERSIONS = [84, 85, 86].freeze
diff --git a/rb/lib/selenium/webdriver/devtools/exception_event.rb b/rb/lib/selenium/webdriver/devtools/exception_event.rb
new file mode 100644
index 0000000000000..df4eb75558bd4
--- /dev/null
+++ b/rb/lib/selenium/webdriver/devtools/exception_event.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+# Licensed to the Software Freedom Conservancy (SFC) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The SFC licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+module Selenium
+ module WebDriver
+ class DevTools
+ class ExceptionEvent
+
+ attr_accessor :description, :timestamp, :stacktrace
+
+ def initialize(description:, timestamp:, stacktrace:)
+ @description = description
+ @timestamp = Time.at(timestamp / 1000)
+ @stacktrace = stacktrace
+ end
+
+ end # ExceptionEvent
+ end # DevTools
+ end # WebDriver
+end # Selenium
diff --git a/rb/spec/integration/selenium/webdriver/devtools_spec.rb b/rb/spec/integration/selenium/webdriver/devtools_spec.rb
index 6f03a151170dd..6aa07a43cf01c 100644
--- a/rb/spec/integration/selenium/webdriver/devtools_spec.rb
+++ b/rb/spec/integration/selenium/webdriver/devtools_spec.rb
@@ -64,7 +64,7 @@ module WebDriver
it 'notifies about log messages' do
logs = []
- driver.on_log_event { |log| logs.push(log) }
+ driver.on_log_event(:console) { |log| logs.push(log) }
driver.navigate.to url_for('javascriptPage.html')
driver.execute_script("console.log('I like cheese');")
@@ -82,6 +82,19 @@ module WebDriver
an_object_having_attributes(type: :log, args: [hash_including('type' => 'object')])
)
end
+
+ it 'notifies about exceptions' do
+ exceptions = []
+ driver.on_log_event(:exception) { |exception| exceptions.push(exception) }
+ driver.navigate.to url_for('javascriptPage.html')
+
+ driver.find_element(id: 'throwing-mouseover').click
+ wait.until { exceptions.any? }
+
+ exception = exceptions.first
+ expect(exception.description).to include('Error: I like cheese')
+ expect(exception.stacktrace).not_to be_empty
+ end
end
end
end