Output Browser logs on failure for Cuprite Capybara driver

This commit is contained in:
Christophe Bliard
2026-02-06 15:47:31 +01:00
parent 0028bb701f
commit 27a677e0c5
2 changed files with 76 additions and 3 deletions
+67 -3
View File
@@ -4,21 +4,79 @@ module Capybara::BrowserLogs
# Capture browser logs on failed examples and output them in Progress and
# Documentation formatters.
class Capture
# Regex matching Ferrum's incoming CDP message format: " ◀ 0.123 {json}"
CDP_INCOMING_MESSAGE_PATTERN = /^\s+◀\s+[\d.]+\s+(.+)$/
class << self
def after_failed_example(example)
return unless failed?(example)
return unless example.example_group.include?(Capybara::DSL)
return if Capybara.page.current_url.blank?
return unless Capybara.page.driver.browser.respond_to?(:manage)
logs = Capybara.page.driver.browser.manage.instance_variable_get(:@bridge).log("browser")
example.metadata[:browser_logs] = logs
logs = extract_logs
example.metadata[:browser_logs] = logs if logs
rescue StandardError => e
warn "Unable to get browser logs: #{e}"
end
private
def extract_logs
if cuprite_driver?
extract_cuprite_logs
elsif selenium_driver?
extract_selenium_logs
end
end
def cuprite_driver?
Capybara.page.driver.is_a?(Capybara::Cuprite::Driver)
end
def selenium_driver?
Capybara.page.driver.browser.respond_to?(:manage)
end
def extract_selenium_logs
Capybara.page.driver.browser.manage.instance_variable_get(:@bridge).log("browser")
end
def extract_cuprite_logs
logger = CupriteCdpLogger.logger
return unless logger
logger.string.each_line.filter_map do |line|
match = line.match(CDP_INCOMING_MESSAGE_PATTERN)
next unless match
parse_console_api_called(match[1])
end
end
def parse_console_api_called(json_string)
data = JSON.parse(json_string)
return unless data["method"] == "Runtime.consoleAPICalled"
params = data["params"]
type = params["type"]
args = params["args"].map { |arg| format_cdp_arg(arg) }
"#{type}: #{args.join(' ')}"
rescue JSON::ParserError
nil
end
def format_cdp_arg(arg)
return arg["value"].to_s if arg.key?("value")
if (preview = arg["preview"]) && (properties = preview["properties"])
formatted = properties.map { |p| "#{p['name']}: #{p['value']}" }.join(", ")
overflow = preview["overflow"] ? ", ..." : ""
return "{#{formatted}#{overflow}}"
end
arg["description"] || arg["type"]
end
# borrowed from capybara-screenshot code
def failed?(example)
return true if example.exception
@@ -59,6 +117,8 @@ module Capybara::BrowserLogs
logs = example.metadata[:browser_logs]
.map(&:to_s)
.grep_v(EXCLUDE_PATTERN)
return if logs.empty?
output.puts(" Browser logs:\n #{logs.join("\n ")}")
end
end
@@ -68,6 +128,10 @@ end
RSpec.configure do |config|
config.after(type: :feature) do |example|
Capybara::BrowserLogs::Capture.after_failed_example(example)
if (logger = CupriteCdpLogger.logger)
logger.truncate(0)
logger.rewind
end
end
config.before(:suite) do
+9
View File
@@ -31,6 +31,12 @@
require "capybara/cuprite"
module CupriteCdpLogger
class << self
attr_accessor :logger
end
end
def headful_mode?
ActiveRecord::Type::Boolean.new.cast(ENV.fetch("OPENPROJECT_TESTING_NO_HEADLESS", nil))
end
@@ -80,6 +86,9 @@ def register_better_cuprite(language, name: :"better_cuprite_#{language}")
options = configure_remote_chrome(options)
CupriteCdpLogger.logger = StringIO.new
options = options.merge(logger: CupriteCdpLogger.logger)
browser_options = {
"disable-dev-shm-usage": nil,
"disable-gpu": nil,