Created
April 5, 2025 14:08
-
-
Save ngan/64f6a3ebf8187b00a274a5edf9bca3cf to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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