From 504676aa3c8ab5f26f727524506def74752b267f Mon Sep 17 00:00:00 2001 From: Paul Keen <125715+pftg@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:04:01 +0200 Subject: [PATCH 1/5] fix: skip at_exit when framework adapter handles finalization Minitest, RSpec, and Cucumber adapters call finalize_reporters! via native hooks. The at_exit fallback in html.rb was firing as a wasted no-op for those frameworks (hitting @finalized guard every time). Now only registers at_exit when no known framework is loaded. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/capybara_screenshot_diff/reporters/html.rb | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/capybara_screenshot_diff/reporters/html.rb b/lib/capybara_screenshot_diff/reporters/html.rb index f3675849..5a3c5118 100644 --- a/lib/capybara_screenshot_diff/reporters/html.rb +++ b/lib/capybara_screenshot_diff/reporters/html.rb @@ -136,6 +136,7 @@ def write_report CapybaraScreenshotDiff.reporters << CapybaraScreenshotDiff::Reporters::HTML.new(embed_images: !!ENV["CI"]) end -# Fallback for frameworks without explicit integration (e.g., Cucumber). -# Minitest and RSpec adapters call finalize_reporters! via their native hooks. +# Always register at_exit as safety net. +# Framework adapters (Minitest, RSpec, Cucumber) also call finalize_reporters! +# via native hooks for correct ordering. The @finalized guard prevents double work. at_exit { CapybaraScreenshotDiff.finalize_reporters! } From 70a629c6bfc45853fad845ccc9ce28db17a17971 Mon Sep 17 00:00:00 2001 From: Paul Keen <125715+pftg@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:21:39 +0200 Subject: [PATCH 2/5] refactor: adopt SimpleCov's external_at_exit pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Framework adapters set external_at_exit = true and register native hooks (Minitest.after_run, RSpec after(:suite), Cucumber AfterAll). The at_exit block checks the flag and skips when a framework adapter handles finalization. This eliminates both the double-call problem and the LIFO ordering problem cleanly — same pattern SimpleCov uses successfully. Co-Authored-By: Claude Opus 4.6 (1M context) --- lib/capybara_screenshot_diff/cucumber.rb | 1 + lib/capybara_screenshot_diff/minitest.rb | 5 ++++- lib/capybara_screenshot_diff/reporters/html.rb | 11 +++++++---- lib/capybara_screenshot_diff/rspec.rb | 1 + lib/capybara_screenshot_diff/screenshot_assertion.rb | 2 ++ 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/lib/capybara_screenshot_diff/cucumber.rb b/lib/capybara_screenshot_diff/cucumber.rb index ea4663bb..4f29a03e 100644 --- a/lib/capybara_screenshot_diff/cucumber.rb +++ b/lib/capybara_screenshot_diff/cucumber.rb @@ -9,4 +9,5 @@ Capybara::Screenshot::BrowserHelpers.resize_window_if_needed end +CapybaraScreenshotDiff.external_at_exit = true AfterAll { CapybaraScreenshotDiff.finalize_reporters! } diff --git a/lib/capybara_screenshot_diff/minitest.rb b/lib/capybara_screenshot_diff/minitest.rb index 519c2d4c..557310fd 100644 --- a/lib/capybara_screenshot_diff/minitest.rb +++ b/lib/capybara_screenshot_diff/minitest.rb @@ -48,4 +48,7 @@ def before_teardown end end -::Minitest.after_run { CapybaraScreenshotDiff.finalize_reporters! } if ::Minitest.respond_to?(:after_run) +if ::Minitest.respond_to?(:after_run) + CapybaraScreenshotDiff.external_at_exit = true + ::Minitest.after_run { CapybaraScreenshotDiff.finalize_reporters! } +end diff --git a/lib/capybara_screenshot_diff/reporters/html.rb b/lib/capybara_screenshot_diff/reporters/html.rb index 5a3c5118..5660c2a1 100644 --- a/lib/capybara_screenshot_diff/reporters/html.rb +++ b/lib/capybara_screenshot_diff/reporters/html.rb @@ -136,7 +136,10 @@ def write_report CapybaraScreenshotDiff.reporters << CapybaraScreenshotDiff::Reporters::HTML.new(embed_images: !!ENV["CI"]) end -# Always register at_exit as safety net. -# Framework adapters (Minitest, RSpec, Cucumber) also call finalize_reporters! -# via native hooks for correct ordering. The @finalized guard prevents double work. -at_exit { CapybaraScreenshotDiff.finalize_reporters! } +# Register at_exit as fallback for frameworks without explicit adapters. +# Framework adapters (Minitest, RSpec, Cucumber) set external_at_exit = true +# and call finalize_reporters! via native hooks for correct ordering. +at_exit do + next if CapybaraScreenshotDiff.external_at_exit? + CapybaraScreenshotDiff.finalize_reporters! +end diff --git a/lib/capybara_screenshot_diff/rspec.rb b/lib/capybara_screenshot_diff/rspec.rb index 653a5278..92e1b835 100644 --- a/lib/capybara_screenshot_diff/rspec.rb +++ b/lib/capybara_screenshot_diff/rspec.rb @@ -42,5 +42,6 @@ end end + CapybaraScreenshotDiff.external_at_exit = true config.after(:suite) { CapybaraScreenshotDiff.finalize_reporters! } end diff --git a/lib/capybara_screenshot_diff/screenshot_assertion.rb b/lib/capybara_screenshot_diff/screenshot_assertion.rb index 78cb4509..be582ffe 100644 --- a/lib/capybara_screenshot_diff/screenshot_assertion.rb +++ b/lib/capybara_screenshot_diff/screenshot_assertion.rb @@ -130,6 +130,8 @@ def reporters end attr_reader :reporters_mutex + attr_accessor :external_at_exit + alias_method :external_at_exit?, :external_at_exit def finalize_reporters! reporters_mutex.synchronize { reporters.dup }.each do |reporter| From ddbf51e8088b0457c197d39f74dab9adaa4b6c91 Mon Sep 17 00:00:00 2001 From: Paul Keen <125715+pftg@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:22:18 +0200 Subject: [PATCH 3/5] docs: add custom framework finalize_reporters! and vips install notes - Document how to call finalize_reporters! for custom test frameworks - Add brew/apt install commands for libvips in Requirements Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8bc3e3ed..4b4ebb3b 100644 --- a/README.md +++ b/README.md @@ -194,9 +194,20 @@ Comparisons add ~50ms per image with VIPS. Without `ruby-vips`, ChunkyPNG is use `DEBUG=1 bundle exec rake test` keeps `.diff.png` files for inspection. +## Custom Test Frameworks + +Minitest, RSpec, and Cucumber are supported out of the box. For other frameworks, call `finalize_reporters!` after all tests complete: + +```ruby +# In your framework's "after suite" hook: +CapybaraScreenshotDiff.finalize_reporters! +``` + +This generates the HTML report and prints the summary. Without this call, the gem falls back to `at_exit` which may fire before your tests finish. + ## Installation -**Requirements:** Ruby 3.2+. Rails 7.1+ for Rails integration; non-Rails projects supported via `CapybaraScreenshotDiff.serve()`. For the `:vips` driver: [libvips 8.9+](https://libvips.github.io/libvips/install.html). +**Requirements:** Ruby 3.2+. Rails 7.1+ for Rails integration; non-Rails projects supported via `CapybaraScreenshotDiff.serve()`. For the `:vips` driver: [libvips 8.9+](https://libvips.github.io/libvips/install.html). On macOS: `brew install vips`. On Ubuntu: `apt-get install libvips-dev`. ## Docs From 974eeaf11c8ff887a80b62aaaaca8763dfcbd21c Mon Sep 17 00:00:00 2001 From: Paul Keen <125715+pftg@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:25:44 +0200 Subject: [PATCH 4/5] refactor: remove external_at_exit flag, rely on @finalized guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The external_at_exit flag was overkill — @finalized in #finalize already prevents double work. Simpler: at_exit always registers, framework adapters also register native hooks, @finalized ensures only the first call does work. Also move custom framework docs from README to docs/framework-setup.md. Co-Authored-By: Claude Opus 4.6 (1M context) --- README.md | 11 ----------- docs/framework-setup.md | 10 ++++++++++ lib/capybara_screenshot_diff/cucumber.rb | 1 - lib/capybara_screenshot_diff/minitest.rb | 5 +---- lib/capybara_screenshot_diff/reporters/html.rb | 10 +++------- lib/capybara_screenshot_diff/rspec.rb | 1 - lib/capybara_screenshot_diff/screenshot_assertion.rb | 2 -- 7 files changed, 14 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 4b4ebb3b..a0b01f79 100644 --- a/README.md +++ b/README.md @@ -194,17 +194,6 @@ Comparisons add ~50ms per image with VIPS. Without `ruby-vips`, ChunkyPNG is use `DEBUG=1 bundle exec rake test` keeps `.diff.png` files for inspection. -## Custom Test Frameworks - -Minitest, RSpec, and Cucumber are supported out of the box. For other frameworks, call `finalize_reporters!` after all tests complete: - -```ruby -# In your framework's "after suite" hook: -CapybaraScreenshotDiff.finalize_reporters! -``` - -This generates the HTML report and prints the summary. Without this call, the gem falls back to `at_exit` which may fire before your tests finish. - ## Installation **Requirements:** Ruby 3.2+. Rails 7.1+ for Rails integration; non-Rails projects supported via `CapybaraScreenshotDiff.serve()`. For the `:vips` driver: [libvips 8.9+](https://libvips.github.io/libvips/install.html). On macOS: `brew install vips`. On Ubuntu: `apt-get install libvips-dev`. diff --git a/docs/framework-setup.md b/docs/framework-setup.md index 72160eb6..578955c0 100644 --- a/docs/framework-setup.md +++ b/docs/framework-setup.md @@ -74,4 +74,14 @@ Then('I should not see any visual difference') do end ``` +## Custom Test Frameworks + +Minitest, RSpec, and Cucumber are supported out of the box. For other frameworks, call `finalize_reporters!` in your framework's "after suite" hook: + +```ruby +CapybaraScreenshotDiff.finalize_reporters! +``` + +This generates the HTML report and prints the summary. Without this call, the gem falls back to `at_exit` which may not fire at the right time depending on your framework's boot order. + [← Back to README](../README.md) diff --git a/lib/capybara_screenshot_diff/cucumber.rb b/lib/capybara_screenshot_diff/cucumber.rb index 4f29a03e..ea4663bb 100644 --- a/lib/capybara_screenshot_diff/cucumber.rb +++ b/lib/capybara_screenshot_diff/cucumber.rb @@ -9,5 +9,4 @@ Capybara::Screenshot::BrowserHelpers.resize_window_if_needed end -CapybaraScreenshotDiff.external_at_exit = true AfterAll { CapybaraScreenshotDiff.finalize_reporters! } diff --git a/lib/capybara_screenshot_diff/minitest.rb b/lib/capybara_screenshot_diff/minitest.rb index 557310fd..519c2d4c 100644 --- a/lib/capybara_screenshot_diff/minitest.rb +++ b/lib/capybara_screenshot_diff/minitest.rb @@ -48,7 +48,4 @@ def before_teardown end end -if ::Minitest.respond_to?(:after_run) - CapybaraScreenshotDiff.external_at_exit = true - ::Minitest.after_run { CapybaraScreenshotDiff.finalize_reporters! } -end +::Minitest.after_run { CapybaraScreenshotDiff.finalize_reporters! } if ::Minitest.respond_to?(:after_run) diff --git a/lib/capybara_screenshot_diff/reporters/html.rb b/lib/capybara_screenshot_diff/reporters/html.rb index 5660c2a1..9988e720 100644 --- a/lib/capybara_screenshot_diff/reporters/html.rb +++ b/lib/capybara_screenshot_diff/reporters/html.rb @@ -136,10 +136,6 @@ def write_report CapybaraScreenshotDiff.reporters << CapybaraScreenshotDiff::Reporters::HTML.new(embed_images: !!ENV["CI"]) end -# Register at_exit as fallback for frameworks without explicit adapters. -# Framework adapters (Minitest, RSpec, Cucumber) set external_at_exit = true -# and call finalize_reporters! via native hooks for correct ordering. -at_exit do - next if CapybaraScreenshotDiff.external_at_exit? - CapybaraScreenshotDiff.finalize_reporters! -end +# Safety net for all frameworks. Framework adapters also call finalize_reporters! +# via native hooks for correct ordering. The @finalized guard prevents double work. +at_exit { CapybaraScreenshotDiff.finalize_reporters! } diff --git a/lib/capybara_screenshot_diff/rspec.rb b/lib/capybara_screenshot_diff/rspec.rb index 92e1b835..653a5278 100644 --- a/lib/capybara_screenshot_diff/rspec.rb +++ b/lib/capybara_screenshot_diff/rspec.rb @@ -42,6 +42,5 @@ end end - CapybaraScreenshotDiff.external_at_exit = true config.after(:suite) { CapybaraScreenshotDiff.finalize_reporters! } end diff --git a/lib/capybara_screenshot_diff/screenshot_assertion.rb b/lib/capybara_screenshot_diff/screenshot_assertion.rb index be582ffe..78cb4509 100644 --- a/lib/capybara_screenshot_diff/screenshot_assertion.rb +++ b/lib/capybara_screenshot_diff/screenshot_assertion.rb @@ -130,8 +130,6 @@ def reporters end attr_reader :reporters_mutex - attr_accessor :external_at_exit - alias_method :external_at_exit?, :external_at_exit def finalize_reporters! reporters_mutex.synchronize { reporters.dup }.each do |reporter| From 047725eb507f8e53d8979d51b1f3bd78946bf0bf Mon Sep 17 00:00:00 2001 From: Paul Keen <125715+pftg@users.noreply.github.com> Date: Mon, 13 Apr 2026 01:31:22 +0200 Subject: [PATCH 5/5] refactor: remove at_exit hook entirely Framework adapters (Minitest, RSpec, Cucumber) call finalize_reporters! via native hooks. at_exit was unreliable due to LIFO ordering and added complexity for a case nobody hits. Custom frameworks can call finalize_reporters! manually (documented in docs/framework-setup.md). Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/framework-setup.md | 2 +- lib/capybara_screenshot_diff/reporters/html.rb | 10 +++------- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/docs/framework-setup.md b/docs/framework-setup.md index 578955c0..0716d169 100644 --- a/docs/framework-setup.md +++ b/docs/framework-setup.md @@ -82,6 +82,6 @@ Minitest, RSpec, and Cucumber are supported out of the box. For other frameworks CapybaraScreenshotDiff.finalize_reporters! ``` -This generates the HTML report and prints the summary. Without this call, the gem falls back to `at_exit` which may not fire at the right time depending on your framework's boot order. +This generates the HTML report and prints the summary. [← Back to README](../README.md) diff --git a/lib/capybara_screenshot_diff/reporters/html.rb b/lib/capybara_screenshot_diff/reporters/html.rb index 9988e720..c04490bd 100644 --- a/lib/capybara_screenshot_diff/reporters/html.rb +++ b/lib/capybara_screenshot_diff/reporters/html.rb @@ -129,13 +129,9 @@ def write_report end end -# Auto-register reporter and at_exit hook. -# The reporter only writes when there are failures (finalize checks failures.empty?). -# Scripts that create their own reporter instance call record/finalize directly. +# Auto-register reporter. +# Framework adapters (Minitest, RSpec, Cucumber) call finalize_reporters! via native hooks. +# For custom frameworks, call CapybaraScreenshotDiff.finalize_reporters! manually. unless CapybaraScreenshotDiff.reporters.any?(CapybaraScreenshotDiff::Reporters::HTML) CapybaraScreenshotDiff.reporters << CapybaraScreenshotDiff::Reporters::HTML.new(embed_images: !!ENV["CI"]) end - -# Safety net for all frameworks. Framework adapters also call finalize_reporters! -# via native hooks for correct ordering. The @finalized guard prevents double work. -at_exit { CapybaraScreenshotDiff.finalize_reporters! }