Check for page errors during snapshot capture
Subscribe to CDP Runtime.consoleAPICalled and
Runtime.exceptionThrown events so each snap raises on
uncaught JS exceptions or console errors/warnings.

Inspired-by: https://alexwlchan.net/2026/playwright/

Assisted-by: GLM-5.1 via pi
change qzmpltoqxsrmryvskxstyrpluokqqxxx
commit 42951a76357b9f48d21ef8a9cd57c8aecb8edaf1
author Alpha Chen <alpha@kejadlen.dev>
date
parent vnvtxwvu
diff --git a/lib/ketchup/snapshots.rb b/lib/ketchup/snapshots.rb
index fe507b5..2c61617 100644
--- a/lib/ketchup/snapshots.rb
+++ b/lib/ketchup/snapshots.rb
@@ -85,11 +85,24 @@ module Ketchup
       end
     end
 
+    # Raised when a snapshot page logs console errors/warnings or throws
+    # uncaught JavaScript exceptions.
+    class PageCheckError < StandardError
+      def initialize(snap_name:, console_errors:, page_errors:)
+        lines = ["Page errors during snap #{snap_name.inspect}:"]
+        console_errors.each { |e| lines << "  console.#{e[:type]}: #{e[:text]}" }
+        page_errors.each { |e| lines << "  page error: #{e}" }
+        super(lines.join("\n"))
+      end
+    end
+
     class Capture
       def initialize(output_dir:, logger: Logger.new($stderr), &server)
         @output_dir = Pathname(output_dir)
         @logger = logger
         @server = server || method(:default_server)
+        @console_messages = []
+        @page_errors = []
       end
 
       def call
@@ -105,6 +118,8 @@ module Ketchup
           browser_options: browser_options
         )
 
+        subscribe_page_errors
+
         @server.call(@browser) do |url|
           @base = url
           @logger.info("Server at #{@base}")
@@ -312,7 +327,7 @@ module Ketchup
       end
 
       def snap(name, selector: nil)
-        yield if block_given?
+        check_page_errors(name) { yield if block_given? }
 
         file = @viewport_dir / "#{name}.png"
         if selector && element_visible?(selector)
@@ -386,7 +401,39 @@ module Ketchup
         Series.first(Sequel.&({ id: noted_ids }, { id: unnoted_ids })) || Series.first
       end
 
+      # Subscribe to CDP events for console messages and uncaught exceptions.
+      # Ferrum subscribes to these internally for its own js_errors/logger
+      # features, but the subscriber supports multiple callbacks.
+      def subscribe_page_errors
+        @browser.on("Runtime.consoleAPICalled") do |params|
+          type = params["type"]
+          text = params["args"].map { |a| a["value"].to_s }.join(" ")
+          @console_messages << { type: type, text: text }
+        end
 
+        @browser.on("Runtime.exceptionThrown") do |params|
+          detail = params.dig("exceptionDetails", "exception", "description")
+          detail ||= params.dig("exceptionDetails", "text")
+          @page_errors << detail.to_s
+        end
+      end
+
+      # Clears error buffers, yields, then raises PageCheckError if the page
+      # logged any console errors/warnings or threw uncaught exceptions.
+      def check_page_errors(snap_name)
+        @console_messages.clear
+        @page_errors.clear
+
+        yield
+
+        console_errors = @console_messages.select { |m| %(error warning).include?(m[:type]) }
+
+        unless console_errors.empty? && @page_errors.empty?
+          raise PageCheckError.new(
+            snap_name:, console_errors:, page_errors: @page_errors.dup
+          )
+        end
+      end
     end
   end
 end