Skip to content

Instantly share code, notes, and snippets.

@ngan
Created April 5, 2025 14:08
Show Gist options
  • Save ngan/64f6a3ebf8187b00a274a5edf9bca3cf to your computer and use it in GitHub Desktop.
Save ngan/64f6a3ebf8187b00a274a5edf9bca3cf to your computer and use it in GitHub Desktop.
require 'concurrent/array'
Capybara::Session.prepend(Spec::Support::Capybara::Console)
module Spec
module Support
module Capybara
module Console
def driver
@driver ||= super.tap { console.register }
end
def reset!
super.tap { console.reset }
end
def console
@console ||= ConsoleManager.new(driver)
end
end
class ConsoleManager
attr_reader :messages
attr_reader :exceptions
def initialize(driver)
@driver = driver
@messages = Concurrent::Array.new
@exceptions = Concurrent::Array.new
end
def register
return if @registered
@registered = true
case @driver
when ::Capybara::Selenium::Driver
# Ideally, we use event handlers but there's a bug with BiDi (web_socket_url) enabled
# and alert modals not being able to appear.
# @driver.browser.script.add_console_message_handler(&method(:handle_console_message))
# @driver.browser.script.add_javascript_error_handler(&method(:handle_javascript_error))
# We'll have to use the legacy API and override the `messages` method.
define_singleton_method(:messages) { fetch_selenium_logs } # rubocop:disable Gusto/NoMetaprogramming
when ::Capybara::Playwright::Driver
# Monkey-patch the Playwright Browser class so that we can register an event handler for
# pages being created. This allows us to register our console/error handlers for each page
# since Playwright treats each page as a separate context.
page_handler = method(:handle_playwright_page)
::Capybara::Playwright::Browser.prepend(
Module.new do
define_method(:create_browser_context) do # rubocop:disable Gusto/NoMetaprogramming
super().tap do |context|
context.on(Playwright::Events::BrowserContext::Page, page_handler)
end
end
# This method is originally private so let's keep it that way.
private :create_browser_context
end
)
end
end
def reset
@messages.clear
@exceptions.clear
end
private
def handle_console_message(message)
@messages <<
case message
when Selenium::WebDriver::LogEntry
parse_selenium_log_entry(message)
when Selenium::WebDriver::BiDi::LogHandler::ConsoleLogEntry
# We don't support this at the moment. See comment above.
message
when Playwright::ConsoleMessage
parse_playwright_console_message(message)
end
end
def handle_javascript_error(error)
@exceptions <<
case error
when Selenium::WebDriver::BiDi::LogHandler::JavaScriptLogEntry
# We don't support this at the moment. See comment above.
error
when Playwright::Error
error
end
end
def handle_playwright_page(page)
page.on(Playwright::Events::Page::Console, method(:handle_console_message))
page.on(Playwright::Events::Page::PageError, method(:handle_javascript_error))
end
def fetch_selenium_logs
@driver.browser.logs.get(:browser).each do |message|
handle_console_message(message)
end
@messages
end
def parse_selenium_log_entry(message)
ConsoleMessage.new(
original: message,
text: message.message,
level: message.level.downcase,
time: Time.zone.at(message.timestamp / 1000.0)
)
end
def parse_playwright_console_message(message)
ConsoleMessage.new(
original: message,
text: message.text,
level: message.type,
# Playwright doesn't give us the timestamp so we just use the current time.
time: Time.current,
url: message.location[:url]
)
end
end
class ConsoleMessage
attr_reader :text
attr_reader :level
attr_reader :time
attr_reader :original
attr_reader :url
def initialize(attributes)
@text = attributes.fetch(:text)
@level = ActiveSupport::StringInquirer.new(attributes.fetch(:level))
@time = attributes.fetch(:time)
@original = attributes.fetch(:original)
@url = attributes[:url]
end
end
class ConsoleException
# To be implemented...
end
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment