From afead7b6c42e4c6d0795e57ef8f6f49b85c7263e Mon Sep 17 00:00:00 2001 From: Alessandro Rodi Date: Tue, 20 Oct 2026 20:54:37 +0200 Subject: [PATCH] Inspect Capybara nodes without walking into the driver A Capybara node holds the session, and through it the driver and the browser connection. Walking its instance variables to build a failure message can take minutes with CDP-based drivers, so nodes are rendered with Capybara's own #inspect. --- Appraisals | 1 + CHANGELOG.md | 4 ++ docs/users/getting-started.md | 12 ++++ gemfiles/rails_7_0_rspec_gte_3_10.gemfile | 1 + gemfiles/rails_7_0_rspec_gte_3_13.gemfile | 1 + gemfiles/rails_7_0_rspec_lt_3_10.gemfile | 1 + gemfiles/rails_7_1_rspec_gte_3_10.gemfile | 1 + gemfiles/rails_7_1_rspec_gte_3_13.gemfile | 1 + gemfiles/rails_7_1_rspec_lt_3_10.gemfile | 1 + gemfiles/rails_7_2_rspec_gte_3_10.gemfile | 1 + gemfiles/rails_7_2_rspec_gte_3_13.gemfile | 1 + gemfiles/rails_7_2_rspec_lt_3_10.gemfile | 1 + gemfiles/rails_8_0_rspec_gte_3_10.gemfile | 1 + gemfiles/rails_8_0_rspec_gte_3_13.gemfile | 1 + gemfiles/rails_8_0_rspec_lt_3_10.gemfile | 1 + gemfiles/rails_8_1_rspec_gte_3_10.gemfile | 1 + gemfiles/rails_8_1_rspec_gte_3_13.gemfile | 1 + gemfiles/rails_8_1_rspec_lt_3_10.gemfile | 1 + lib/super_diff/capybara.rb | 13 ++++ .../capybara/inspection_tree_builders.rb | 12 ++++ .../capybara/inspection_tree_builders/node.rb | 33 ++++++++++ lib/super_diff/rspec.rb | 2 + spec/spec_helper.rb | 7 +++ spec/unit/capybara/object_inspection_spec.rb | 63 +++++++++++++++++++ 24 files changed, 162 insertions(+) create mode 100644 lib/super_diff/capybara.rb create mode 100644 lib/super_diff/capybara/inspection_tree_builders.rb create mode 100644 lib/super_diff/capybara/inspection_tree_builders/node.rb create mode 100644 spec/unit/capybara/object_inspection_spec.rb diff --git a/Appraisals b/Appraisals index 6205bd9d..be5a39cb 100644 --- a/Appraisals +++ b/Appraisals @@ -6,6 +6,7 @@ rails_dependencies = gem 'jdbc-sqlite3', platform: :jruby gem 'net-ftp' gem 'combustion' + gem 'capybara' end appraisals = { diff --git a/CHANGELOG.md b/CHANGELOG.md index d8b9ef6b..bdff02c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Features + +- Inspect Capybara nodes without walking into the driver. + ### Other changes - Fixed "Using parts of SuperDiff directly" documentation. [#313](https://github.com/splitwise/super_diff/pull/313) diff --git a/docs/users/getting-started.md b/docs/users/getting-started.md index ad0bd599..c990974a 100644 --- a/docs/users/getting-started.md +++ b/docs/users/getting-started.md @@ -86,6 +86,18 @@ such as matchers. You can now continue on to [customizing SuperDiff](./customization.md). +## Capybara + +A Capybara node holds the session, and through it the driver and the browser. +SuperDiff prints nodes the way Capybara inspects them +instead of walking into the browser connection. +This is loaded by `super_diff/rspec` when Capybara is already loaded; +otherwise, add: + +```ruby +require "super_diff/capybara" +``` + ## Binary Strings SuperDiff can diff binary strings (`Encoding::ASCII_8BIT`) using a hex-dump diff --git a/gemfiles/rails_7_0_rspec_gte_3_10.gemfile b/gemfiles/rails_7_0_rspec_gte_3_10.gemfile index 802d39ab..cc385812 100644 --- a/gemfiles/rails_7_0_rspec_gte_3_10.gemfile +++ b/gemfiles/rails_7_0_rspec_gte_3_10.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 7.0.0" gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw] gem "rspec", "3.12.0" diff --git a/gemfiles/rails_7_0_rspec_gte_3_13.gemfile b/gemfiles/rails_7_0_rspec_gte_3_13.gemfile index 0a8e2319..4bd91671 100644 --- a/gemfiles/rails_7_0_rspec_gte_3_13.gemfile +++ b/gemfiles/rails_7_0_rspec_gte_3_13.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 7.0.0" gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw] gem "rspec", "3.13.0" diff --git a/gemfiles/rails_7_0_rspec_lt_3_10.gemfile b/gemfiles/rails_7_0_rspec_lt_3_10.gemfile index 9d13bcc5..5bc94889 100644 --- a/gemfiles/rails_7_0_rspec_lt_3_10.gemfile +++ b/gemfiles/rails_7_0_rspec_lt_3_10.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 7.0.0" gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw] gem "rspec", "~> 3.9.0" diff --git a/gemfiles/rails_7_1_rspec_gte_3_10.gemfile b/gemfiles/rails_7_1_rspec_gte_3_10.gemfile index 6d706dbf..8ca3d587 100644 --- a/gemfiles/rails_7_1_rspec_gte_3_10.gemfile +++ b/gemfiles/rails_7_1_rspec_gte_3_10.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 7.1.0" gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw] gem "rspec", "3.12.0" diff --git a/gemfiles/rails_7_1_rspec_gte_3_13.gemfile b/gemfiles/rails_7_1_rspec_gte_3_13.gemfile index d2b95e0e..df52730d 100644 --- a/gemfiles/rails_7_1_rspec_gte_3_13.gemfile +++ b/gemfiles/rails_7_1_rspec_gte_3_13.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 7.1.0" gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw] gem "rspec", "3.13.0" diff --git a/gemfiles/rails_7_1_rspec_lt_3_10.gemfile b/gemfiles/rails_7_1_rspec_lt_3_10.gemfile index 79b2b235..6dae509d 100644 --- a/gemfiles/rails_7_1_rspec_lt_3_10.gemfile +++ b/gemfiles/rails_7_1_rspec_lt_3_10.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 7.1.0" gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw] gem "rspec", "~> 3.9.0" diff --git a/gemfiles/rails_7_2_rspec_gte_3_10.gemfile b/gemfiles/rails_7_2_rspec_gte_3_10.gemfile index 467d85e1..50a2db8b 100644 --- a/gemfiles/rails_7_2_rspec_gte_3_10.gemfile +++ b/gemfiles/rails_7_2_rspec_gte_3_10.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 7.2.0" gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw] gem "rspec", "3.12.0" diff --git a/gemfiles/rails_7_2_rspec_gte_3_13.gemfile b/gemfiles/rails_7_2_rspec_gte_3_13.gemfile index db7e45b9..046ee8f9 100644 --- a/gemfiles/rails_7_2_rspec_gte_3_13.gemfile +++ b/gemfiles/rails_7_2_rspec_gte_3_13.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 7.2.0" gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw] gem "rspec", "3.13.0" diff --git a/gemfiles/rails_7_2_rspec_lt_3_10.gemfile b/gemfiles/rails_7_2_rspec_lt_3_10.gemfile index 4efc99ad..91dab74f 100644 --- a/gemfiles/rails_7_2_rspec_lt_3_10.gemfile +++ b/gemfiles/rails_7_2_rspec_lt_3_10.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 7.2.0" gem "sqlite3", "~> 1.4", platform: [:ruby, :mswin, :mingw] gem "rspec", "~> 3.9.0" diff --git a/gemfiles/rails_8_0_rspec_gte_3_10.gemfile b/gemfiles/rails_8_0_rspec_gte_3_10.gemfile index 3db3112d..c671b7e8 100644 --- a/gemfiles/rails_8_0_rspec_gte_3_10.gemfile +++ b/gemfiles/rails_8_0_rspec_gte_3_10.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 8.0.0" gem "sqlite3", ">= 2.1", platform: [:ruby, :mswin, :mingw] gem "rspec", "3.12.0" diff --git a/gemfiles/rails_8_0_rspec_gte_3_13.gemfile b/gemfiles/rails_8_0_rspec_gte_3_13.gemfile index 17e780b3..0ab8e7b9 100644 --- a/gemfiles/rails_8_0_rspec_gte_3_13.gemfile +++ b/gemfiles/rails_8_0_rspec_gte_3_13.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 8.0.0" gem "sqlite3", ">= 2.1", platform: [:ruby, :mswin, :mingw] gem "rspec", "3.13.0" diff --git a/gemfiles/rails_8_0_rspec_lt_3_10.gemfile b/gemfiles/rails_8_0_rspec_lt_3_10.gemfile index 8279725a..3a94355a 100644 --- a/gemfiles/rails_8_0_rspec_lt_3_10.gemfile +++ b/gemfiles/rails_8_0_rspec_lt_3_10.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 8.0.0" gem "sqlite3", ">= 2.1", platform: [:ruby, :mswin, :mingw] gem "rspec", "~> 3.9.0" diff --git a/gemfiles/rails_8_1_rspec_gte_3_10.gemfile b/gemfiles/rails_8_1_rspec_gte_3_10.gemfile index 33bfc29c..fdbd5e94 100644 --- a/gemfiles/rails_8_1_rspec_gte_3_10.gemfile +++ b/gemfiles/rails_8_1_rspec_gte_3_10.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 8.1.0" gem "sqlite3", ">= 2.1", platform: [:ruby, :mswin, :mingw] gem "rspec", "3.12.0" diff --git a/gemfiles/rails_8_1_rspec_gte_3_13.gemfile b/gemfiles/rails_8_1_rspec_gte_3_13.gemfile index 8541321e..d7555bbc 100644 --- a/gemfiles/rails_8_1_rspec_gte_3_13.gemfile +++ b/gemfiles/rails_8_1_rspec_gte_3_13.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 8.1.0" gem "sqlite3", ">= 2.1", platform: [:ruby, :mswin, :mingw] gem "rspec", "3.13.0" diff --git a/gemfiles/rails_8_1_rspec_lt_3_10.gemfile b/gemfiles/rails_8_1_rspec_lt_3_10.gemfile index f19cb1de..3604fdba 100644 --- a/gemfiles/rails_8_1_rspec_lt_3_10.gemfile +++ b/gemfiles/rails_8_1_rspec_lt_3_10.gemfile @@ -14,6 +14,7 @@ gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "jdbc-sqlite3", platform: :jruby gem "net-ftp" gem "combustion" +gem "capybara" gem "rails", "~> 8.1.0" gem "sqlite3", ">= 2.1", platform: [:ruby, :mswin, :mingw] gem "rspec", "~> 3.9.0" diff --git a/lib/super_diff/capybara.rb b/lib/super_diff/capybara.rb new file mode 100644 index 00000000..14c4c25c --- /dev/null +++ b/lib/super_diff/capybara.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require 'super_diff/capybara/inspection_tree_builders' + +module SuperDiff + module Capybara + SuperDiff.configure do |config| + config.prepend_extra_inspection_tree_builder_classes( + InspectionTreeBuilders::Node + ) + end + end +end diff --git a/lib/super_diff/capybara/inspection_tree_builders.rb b/lib/super_diff/capybara/inspection_tree_builders.rb new file mode 100644 index 00000000..bb5a6586 --- /dev/null +++ b/lib/super_diff/capybara/inspection_tree_builders.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module SuperDiff + module Capybara + module InspectionTreeBuilders + autoload( + :Node, + 'super_diff/capybara/inspection_tree_builders/node' + ) + end + end +end diff --git a/lib/super_diff/capybara/inspection_tree_builders/node.rb b/lib/super_diff/capybara/inspection_tree_builders/node.rb new file mode 100644 index 00000000..358d5667 --- /dev/null +++ b/lib/super_diff/capybara/inspection_tree_builders/node.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module SuperDiff + module Capybara + module InspectionTreeBuilders + # A node holds the session, and through it the driver and the browser + # connection. Walking its instance variables can take minutes, so it is + # rendered the way Capybara prints it. + class Node < Core::AbstractInspectionTreeBuilder + def self.applies_to?(value) + value.is_a?(::Capybara::Node::Base) || + value.is_a?(::Capybara::Driver::Node) + end + + def call + Core::InspectionTree.new do |t1| + t1.as_lines_when_rendering_to_lines do |t2| + t2.add_text inspection + end + end + end + + private + + def inspection + object.inspect + rescue StandardError + "#<#{object.class.name}>" + end + end + end + end +end diff --git a/lib/super_diff/rspec.rb b/lib/super_diff/rspec.rb index a6312215..875e1675 100644 --- a/lib/super_diff/rspec.rb +++ b/lib/super_diff/rspec.rb @@ -134,4 +134,6 @@ def self.rspec_version require_relative 'rspec/monkey_patches' +require 'super_diff/capybara' if defined?(Capybara) + RSpec.configuration.filter_gems_from_backtrace('super_diff') diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 33167965..83aed2e1 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -31,6 +31,12 @@ # ActionDispatch isn't available (presumably because we're not testing it) end +begin + require 'capybara' +rescue LoadError + # Capybara isn't available (presumably because we're not testing it) +end + Dir .glob(File.expand_path('support/**/*.rb', __dir__)) .reject { |file| file.include?('/models/active_record/') && !active_record_available } @@ -59,6 +65,7 @@ config.filter_run_excluding active_record: true unless active_record_available config.filter_run_excluding action_dispatch: true unless defined?(ActionDispatch) + config.filter_run_excluding capybara: true unless defined?(Capybara) config.filter_run_excluding active_support: true unless defined?(ActiveSupport) config.filter_run_excluding with_superdiff_rspec: false diff --git a/spec/unit/capybara/object_inspection_spec.rb b/spec/unit/capybara/object_inspection_spec.rb new file mode 100644 index 00000000..c7011cf5 --- /dev/null +++ b/spec/unit/capybara/object_inspection_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SuperDiff, type: :unit do + describe '.inspect_object', 'for Capybara objects', capybara: true do + let(:session) do + app = ->(_env) { [200, { 'content-type' => 'text/html' }, ['

Hello

']] } + Capybara::Session.new(:rack_test, app).tap { |s| s.visit('/') } + end + + context 'given a Capybara::Node::Element' do + context 'given as_lines: false' do + it 'returns the inspection Capybara gives it' do + string = + described_class.inspect_object(session.find(:css, 'h1'), as_lines: false) + expect(string).to eq( + %(#) + ) + end + end + + context 'given as_lines: true' do + it 'returns the inspection Capybara gives it on one line' do + tiered_lines = + described_class.inspect_object( + session.find(:css, 'h1'), + as_lines: true, + type: :noop, + indentation_level: 1 + ) + + expect(tiered_lines).to match( + [ + an_object_having_attributes( + type: :noop, + indentation_level: 1, + value: %(#) + ) + ] + ) + end + end + end + + context 'given a Capybara::Driver::Node' do + it 'returns the inspection the driver gives it' do + node = session.find(:css, 'h1').base + string = described_class.inspect_object(node, as_lines: false) + expect(string).to eq(node.inspect) + end + end + + context 'given a node whose inspection raises' do + it 'returns the class name' do + node = session.find(:css, 'h1') + allow(node).to receive(:inspect).and_raise(StandardError) + string = described_class.inspect_object(node, as_lines: false) + expect(string).to eq('#') + end + end + end +end