diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 2958c4ad..1208e936 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -37,7 +37,7 @@ If applicable, add the `.diff.png` or `.heatmap.diff.png` files to help explain
- **Ruby version:** (e.g., 3.4.1)
- **Rails version:** (e.g., 8.0) or N/A (non-Rails project)
- **`capybara-screenshot-diff` version:** (e.g., 1.12.0)
-- **Image processing driver:** (`:vips` or `:chunky_png`)
+- **libvips version:** (`vips --version`; the only image backend since 2.1)
- **Capybara driver:** (e.g., `selenium_chrome_headless`, `cuprite`)
- **Operating system:** (e.g., macOS 14, Ubuntu 24.04)
- **CI environment:** (e.g., GitHub Actions, local only)
@@ -60,5 +60,5 @@ DEBUG=1 bundle exec rake test
Add any other context about the problem here. For example:
- Is it specific to CI vs local?
-- Does it reproduce with both VIPS and ChunkyPNG drivers?
+- Does it reproduce with a different libvips version?
- Is this a regression from a previous version?
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index da99c0dc..40c98cb1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -29,7 +29,7 @@ jobs:
- name: Verify version
run: |
GEM_VERSION="${{ github.event.inputs.version }}"
- CODE_VERSION=$(ruby -I lib -r capybara/screenshot/diff/version -e "puts Capybara::Screenshot::Diff::VERSION")
+ CODE_VERSION=$(ruby -I lib -r snap_diff/version -e "puts SnapDiff::VERSION")
if [ "$GEM_VERSION" != "$CODE_VERSION" ]; then
echo "Version mismatch: input=$GEM_VERSION code=$CODE_VERSION"
exit 1
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9c01c8dd..9f1f77d3 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -57,8 +57,6 @@ jobs:
ruby-version: "4.0"
- run: bin/rake test
- env:
- SCREENSHOT_DRIVER: vips
functional-test:
name: Functional Test
@@ -81,7 +79,6 @@ jobs:
env:
COVERAGE: enabled
DISABLE_SKIP_TESTS: 1
- SCREENSHOT_DRIVER: vips
- uses: ./.github/actions/upload-screenshots
if: failure()
@@ -137,13 +134,9 @@ jobs:
env:
BUNDLE_GEMFILE: gemfiles/${{ matrix.gemfile }}
- # The JRuby cells are the only place the vips driver runs on a non-MRI
- # engine, and `bin/rake test` otherwise leaves them on chunky_png (the
- # default in test/system_test_case.rb) -- so ruby-vips' FFI path was
- # only ever loaded there, never driven end to end. Test Drivers covers
- # both drivers on CRuby; chunky_png stays covered on JRuby by its own
- # unit tests, which do not read this variable.
- SCREENSHOT_DRIVER: ${{ contains(matrix.ruby-version, 'jruby') && 'vips' || 'chunky_png' }}
+ # SCREENSHOT_DRIVER is gone: 2.1 removed the driver abstraction, so every
+ # cell -- JRuby included -- runs libvips. ruby-vips' FFI path on JRuby is
+ # now a property of the whole matrix rather than of one variable.
steps:
- uses: actions/checkout@v7
@@ -173,7 +166,10 @@ jobs:
max_attempts: 2
command: bin/rake test
- matrix-screenshot-driver:
+ # Was `matrix-screenshot-driver`, a capybara-driver x screenshot-driver grid.
+ # 2.1 removed the screenshot-driver axis, so this is a capybara-driver matrix
+ # and nothing else -- half the cells, same coverage.
+ matrix-capybara-driver:
name: Test Drivers
# Cost-intentional: full matrix stays off PRs by default (free-tier
# Actions minutes). Runs on master pushes, manual dispatch, the weekly
@@ -188,7 +184,6 @@ jobs:
strategy:
matrix:
capybara-driver: [ selenium_headless, selenium_chrome_headless, cuprite ]
- screenshot-driver: [ vips, chunky_png ]
runs-on: ubuntu-latest
@@ -200,7 +195,8 @@ jobs:
- uses: ./.github/actions/setup-ruby-and-dependencies
with:
ruby-version: "4.0"
- cache-apt-packages: ${{ matrix.screenshot-driver == 'vips' }}
+ # libvips is required now, not one of two options.
+ cache-apt-packages: true
- name: Cache Selenium
uses: actions/cache@v6
@@ -211,12 +207,11 @@ jobs:
- run: bin/rake test:integration
env:
CAPYBARA_DRIVER: ${{ matrix.capybara-driver }}
- SCREENSHOT_DRIVER: ${{ matrix.screenshot-driver }}
- uses: ./.github/actions/upload-screenshots
if: failure()
with:
- name: screenshots-${{ matrix.capybara-driver }}-${{ matrix.screenshot-driver }}
+ name: screenshots-${{ matrix.capybara-driver }}
test-report-upload:
name: Test Report Upload
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 581f276e..923a079d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -9,7 +9,7 @@ to adhere to the [Contributor Covenant](CODE_OF_CONDUCT.md) code of conduct.
### Prerequisites
- **Ruby 3.2+** (the project tests against 3.2–4.0)
-- **libvips 8.9+** (optional, for the VIPS driver). Install with:
+- **libvips 8.9+** — required. It is the only image backend, and `ruby-vips` is a runtime dependency of the gem. Install the system library with:
- macOS: `brew install vips`
- Ubuntu: `sudo apt-get install libvips-dev`
- **Chrome** (for integration tests with `selenium_chrome_headless` or `cuprite`)
@@ -37,9 +37,6 @@ rake test:integration
# Run specific test file
ruby -Ilib:test test/unit/image_compare_test.rb
-# Run with a specific screenshot driver
-SCREENSHOT_DRIVER=vips rake test
-
# Run with a specific Capybara driver (integration tests)
CAPYBARA_DRIVER=cuprite rake test:integration
@@ -90,9 +87,9 @@ The gem supports Ruby 3.2 through 4.0 (including JRuby). When adding features:
### Architecture patterns
-- **Value objects** — immutable data carriers (e.g., `Difference`, `Comparison`, `Region`)
-- **Strategy pattern** — interchangeable algorithms (e.g., `VipsDriver`/`ChunkyPNGDriver`)
-- **Layered comparison** — fast-then-slow strategy in `ImageCompare` (byte → pixel → region)
+- **Value objects** — immutable data carriers (e.g., `ComparisonResult`, `Comparison::Images`, `Region`)
+- **Layered comparison** — fast-then-slow strategy in `SnapDiff::Comparison` (byte → pixel → region)
+- **One image backend** — `SnapDiff::Drivers::VipsDriver`. 2.1 removed the driver abstraction; do not reintroduce a strategy layer around it
- **Thread safety** — thread-local state for per-test data, mutex for shared state
- **Test doubles** — use `TestDoubles::TestDriver` and `TestDoubles::TestPath` (see `test/support/test_doubles.rb`)
@@ -130,29 +127,21 @@ Include:
- **Unit tests** go in `test/unit/` and test a single class in isolation. Use test doubles from `test/support/test_doubles.rb` rather than testing with real image files or browsers.
- **Integration tests** go in `test/integration/` and exercise the full capture → compare → report pipeline with a real browser. These are slower and require Chrome.
-- **Driver contract tests** (`test/support/driver_contract_tests.rb`) verify that all image processing drivers meet the same interface. Add a contract test when adding a new driver method.
-- **Test environment isolation:** each unit test snapshots and restores `Capybara::Screenshot` and `Capybara::Screenshot::Diff` global state. Don't mutate globals outside of setup/teardown.
-
-## Adding a New Driver
-
-1. Create `lib/capybara/screenshot/diff/drivers/new_driver.rb` inheriting from `BaseDriver`
-2. Implement required methods: `load_images`, `from_file`, `save_image_to`, `same_pixels?`, `find_difference_region`, `crop`, `add_black_box`, `draw_rectangles`, `resize_image_to`
-3. Register in `Utils.detect_available_drivers` and `Utils.find_driver_class_for`
-4. Add driver contract tests in `test/unit/drivers/new_driver_test.rb`
-5. Add integration tests exercising the new driver
+- **Driver contract tests** (`test/support/driver_contract_tests.rb`) pin the interface `Comparison`, `ImagePreprocessor`, `Screenshoter` and `AnnotationService` all call on `VipsDriver` — signatures, arity, and `load_images` slot order. It is no longer a *shared* contract (2.1 left one backend), but drift in any of those would break the callers silently. Extend it when you change a method the callers use.
+- **Test environment isolation:** each unit test snapshots and restores `SnapDiff.config` by instance variable. Don't mutate global config outside of setup/teardown.
## Adding a New Reporter
-1. Create `lib/capybara_screenshot_diff/reporters/new_reporter.rb`
-2. Implement `record(assertions)` and `finalize` methods
-3. Register with `CapybaraScreenshotDiff.reporters << MyReporter.new`
+1. Create `lib/snap_diff/reporters/new_reporter.rb`
+2. Implement `record(assertions)`, `finalize` and `summary` methods
+3. Register with `SnapDiff::Reporting.register(MyReporter.new)`
4. Add tests in `test/unit/reporters/`
## Releasing
To release a new version:
-1. Update the version number in [lib/capybara/screenshot/diff/version.rb](lib/capybara/screenshot/diff/version.rb)
+1. Update the version number in [lib/snap_diff/version.rb](lib/snap_diff/version.rb)
2. Update [CHANGELOG.md](CHANGELOG.md) with the new version and date
3. Create a GitHub Release:
- Go to [Actions → Release](https://github.com/snap-diff/snap_diff-capybara/actions/workflows/release.yml)
diff --git a/README.md b/README.md
index 909e337a..b27d9229 100644
--- a/README.md
+++ b/README.md
@@ -3,7 +3,7 @@
[](https://github.com/snap-diff/snap_diff-capybara/actions/workflows/test.yml)
[](https://deepwiki.com/snap-diff/snap_diff-capybara)
-# Capybara::Screenshot::Diff
+# SnapDiff for Capybara
Stop shipping UI bugs. Take screenshots in your Capybara tests, commit baselines to git, and let CI catch visual regressions in pull requests — no cloud service, no subscription, runs entirely in your test suite.
@@ -11,9 +11,9 @@ Stop shipping UI bugs. Take screenshots in your Capybara tests, commit baselines
**Why this gem?** Baselines live in git — review UI changes in pull requests like you review code. Runs offline, works in CI, zero vendor lock-in. Unlike Percy/Chromatic (paid SaaS), nothing to sign up for. Unlike BackstopJS, no Node required.
-> **2.0 experiment (beta):** the gem is moving to a `SnapDiff` canonical namespace. Opt in with `gem "capybara-screenshot-diff", "2.0.0.beta3"` (or the latest 2.0.0 prerelease; prereleases are never installed by default — normal installs stay on 1.x). Legacy names keep working; the first legacy API a process touches prints one migration notice (lazily shimmed constants also warn once each — see [which names warn](docs/UPGRADING.md#deprecation-warnings)), silenceable via `SnapDiff.silence_deprecations = true` or `SNAP_DIFF_SILENCE_DEPRECATIONS=1`. Writing new code? Start from [SnapDiff — the canonical API](docs/snapdiff.md), which uses canonical names only. Migrating an existing suite? See the [upgrade guide](docs/UPGRADING.md). Share feedback on [#166](https://github.com/snap-diff/snap_diff-capybara/issues/166).
+> **2.1 removed the v1 API.** Everything lives under `SnapDiff` now — the `Capybara::Screenshot::Diff` and `CapybaraScreenshotDiff` namespaces, the `capybara_screenshot_diff/*` require paths, the ChunkyPNG driver, the `driver:` setting and `shift_distance_limit` are gone, not deprecated. 2.0 was the transitional release where both APIs worked and everything that died warned. Coming from 1.x or 2.0? The [upgrade guide](docs/UPGRADING.md) has the change list; the real-world migration was 17 lines across two files.
>
-> Starting with the 2.0 prereleases the gem is also published as [`snap_diff-capybara`](https://rubygems.org/gems/snap_diff-capybara) — identical content and versions under the forward-looking name, matching this repository. Install either; don't install both.
+> The gem is published under two names — [`snap_diff-capybara`](https://rubygems.org/gems/snap_diff-capybara) (matching this repository) and [`capybara-screenshot-diff`](https://rubygems.org/gems/capybara-screenshot-diff) — with identical content and versions. Install either; don't install both.
## Quick Start (5 minutes)
@@ -21,19 +21,20 @@ Stop shipping UI bugs. Take screenshots in your Capybara tests, commit baselines
```ruby
# Gemfile
-gem 'capybara-screenshot-diff'
-gem 'ruby-vips' # Optional: 10x faster comparisons
+gem 'snap_diff-capybara'
+# ruby-vips comes with the gem since 2.1; libvips itself is a system package
+# (brew install vips / apt-get install libvips).
```
```ruby
# test/test_helper.rb
-require 'capybara_screenshot_diff/minitest'
+require 'snap_diff/integrations/minitest'
```
```ruby
# test/application_system_test_case.rb
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
- include CapybaraScreenshotDiff::Minitest::Assertions
+ include SnapDiff::Minitest::Assertions
end
```
@@ -86,8 +87,8 @@ For RSpec, Cucumber, or non-Rails setup, see [Framework Setup](docs/framework-se
### For Non-Rails Projects (Hugo, Jekyll, Static Sites)
```ruby
-require 'capybara_screenshot_diff/static'
-CapybaraScreenshotDiff.serve("_site") # or "public", "build", "dist"
+require 'snap_diff/static'
+SnapDiff.serve("_site") # or "public", "build", "dist"
```
Then commit baselines to git just like Rails. [Full setup](docs/ci-integration.md#non-rails-projects-hugo-jekyll-static-sites).
@@ -98,7 +99,7 @@ The test fails with a clear message and generates diff files:
```text
Screenshot does not match for 'homepage':
-({"area_size":1250,"region":[0,19,199,83],"max_color_distance":42.5})
+({"area_size":684.0,"region":[11.0,3.0,49.0,21.0],"difference_level":0.0653125})
```
Open `doc/screenshots/homepage.diff.png` to see exactly what changed. If the change is intentional, delete the baseline and re-run to update it.
@@ -115,7 +116,7 @@ Add one line to get an interactive dashboard for reviewing all screenshot differ
```ruby
# test/test_helper.rb
-require 'capybara_screenshot_diff/reporters/html'
+require 'snap_diff/reporters/html'
```
After tests run, open `doc/screenshots/snap_diff_report.html`:
@@ -129,7 +130,9 @@ See [Web UI & Custom Reporters](docs/reporters.md) for full feature details and
Works without a browser — PDFs, generated images, CI artifacts:
```ruby
-result = Capybara::Screenshot::Diff.compare("baseline.png", "current.png")
+require 'snap_diff'
+
+result = SnapDiff.compare("baseline.png", "current.png")
result.different? # => true if visually different
result.quick_equal? # => true if byte-identical
```
@@ -138,8 +141,8 @@ result.quick_equal? # => true if byte-identical
- **Crop to element:** `screenshot "form", crop: "#main-form"`
- **Ignore regions:** `screenshot "dashboard", skip_area: [".timestamp"]`
-- **Disable animations:** `Capybara::Screenshot.disable_animations = true`
-- **Set window size:** `Capybara::Screenshot.window_size = [1280, 1024]`
+- **Disable animations:** `SnapDiff.config.disable_animations = true`
+- **Set window size:** `SnapDiff.config.window_size = [1280, 1024]`
## Handling Flaky Tests
@@ -148,10 +151,10 @@ Defaults work for most Rails apps — `blur_active_element`, `hide_caret`, and `
If screenshots differ between CI and local, set a comparison threshold:
```ruby
-Capybara::Screenshot::Diff.configure do |screenshot, diff|
- screenshot.window_size = [1280, 1024] # consistent viewport
- diff.perceptual_threshold = 2.0 # ignore anti-aliasing (VIPS only)
- # or: diff.tolerance = 0.001 # percentage-based (default for VIPS)
+SnapDiff.configure do |config|
+ config.window_size = [1280, 1024] # consistent viewport
+ config.perceptual_threshold = 2.0 # ignore anti-aliasing
+ # or: config.tolerance = 0.001 # percentage-based (the default)
end
```
@@ -174,7 +177,7 @@ Delete the baseline file and re-run tests: `rm doc/screenshots/homepage.png && b
CSS animations make my screenshots flaky
-Enable `Capybara::Screenshot.disable_animations = true` to freeze CSS animations/transitions before each capture. Or use `stability_time_limit: 1` to wait for animations to finish.
+Enable `SnapDiff.config.disable_animations = true` to freeze CSS animations/transitions before each capture. Or use `stability_time_limit: 1` to wait for animations to finish.
@@ -186,7 +189,7 @@ Set `window_size` for consistent dimensions and use `perceptual_threshold: 2.0`
Will this slow down my tests?
-Comparisons add ~50ms per image with VIPS. Without `ruby-vips`, ChunkyPNG is used (slower but no system dependency). `stability_time_limit` adds wait time — keep it low (0.1-0.5s) or use `disable_animations` instead.
+Comparisons add ~50ms per image. `stability_time_limit` adds wait time — keep it low (0.1-0.5s) or use `disable_animations` instead.
@@ -197,15 +200,15 @@ Comparisons add ~50ms per image with VIPS. Without `ruby-vips`, ChunkyPNG is use
## 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`.
+**Requirements:** Ruby 3.2+. Rails 7.1+ for Rails integration; non-Rails projects supported via `SnapDiff.serve()`. Comparison runs on [libvips](https://libvips.github.io/libvips/install.html) (8.9+), a system package: `brew install vips` on macOS, `apt-get install libvips-dev` on Ubuntu. The `ruby-vips` binding is a runtime dependency of this gem since 2.1, so Bundler installs it for you.
## Docs
-- [SnapDiff — the canonical API](docs/snapdiff.md) — setup, config, object map, custom drivers & reporters, canonical names only
+- [SnapDiff — the canonical API](docs/snapdiff.md) — setup, config, object map, custom reporters
- [Framework Setup](docs/framework-setup.md) — Minitest, RSpec, Cucumber
- [CI & Non-Rails Integration](docs/ci-integration.md) — GitHub Actions, reusable action, static sites, baseline updates
- [Configuration Reference](docs/configuration.md) — all options explained
-- [Image Processing Drivers](docs/drivers.md) — VIPS, ChunkyPNG, perceptual threshold
+- [Image Processing](docs/drivers.md) — libvips, perceptual threshold, tolerance
- [Screenshot Organization](docs/organization.md) — groups, sections, cropping, multi-browser
- [Web UI & Custom Reporters](docs/reporters.md) — interactive report, custom reporters
@@ -215,7 +218,7 @@ After checking out the repo, run `bin/setup` then `rake test`. See [Docker Testi
## Contributing
-See [CONTRIBUTING.md](CONTRIBUTING.md)
+See [CONTRIBUTING.md](https://github.com/snap-diff/snap_diff-capybara/blob/master/CONTRIBUTING.md)
## License
diff --git a/Rakefile b/Rakefile
index c0c705ed..60cafba5 100644
--- a/Rakefile
+++ b/Rakefile
@@ -5,46 +5,24 @@ require "rake/testtask"
task default: :test
-# THE 3.0 SPLIT.
+# `test:canonical` was "everything except test/legacy/", i.e. what had to
+# still pass once the v1 surface was deleted. 2.1 deleted it, test/legacy/
+# went with it, and the two tasks converged on the same file list -- so the
+# second name is gone rather than kept as an alias for one thing.
#
-# test/legacy/ holds every test whose SUBJECT is the v1 compatibility surface
-# -- the old Capybara::Screenshot / CapybaraScreenshotDiff namespaces, their
-# deprecation warnings, and the gates that keep lib/capybara* alias-only.
-# Those tests guard the v1 contract for the whole 2.x line, so they stay and
-# stay green; in 3.0 they are deleted by the same commit that deletes what
-# they test:
-#
-# git rm -r lib/capybara* lib/capybara_screenshot_diff.rb \
-# lib/snap_diff/legacy_shims.rb lib/snap_diff/deprecation.rb \
-# test/legacy
-#
-# A directory rather than a list in this file: there is nothing to keep in
-# sync, and the deletion is one `git rm -r`.
-#
-# `rake test` -- everything, today's gate.
-# `rake test:canonical` -- exactly what must still pass once test/legacy and
-# the v1 trees are gone. THE 3.0 GATE.
-# `rake test:unit` -- unit-sized tests; test/legacy is unit-sized too
-# (legacy/ marks lifetime, not kind), so it is in.
-LEGACY_SURFACE_TESTS = "test/legacy/**/*_test.rb"
-
+# `rake test` -- THE gate.
+# `rake test:unit` -- unit-sized tests.
+# `rake test:integration` -- browser-driven tests.
Rake::TestTask.new(:test) do |t|
t.libs << "test"
t.libs << "lib"
t.test_files = FileList["test/**/*_test.rb"]
end
-desc "Run every test that must survive the 3.0 deletion of the v1 surface"
-Rake::TestTask.new("test:canonical") do |t|
- t.libs << "test"
- t.libs << "lib"
- t.test_files = FileList["test/**/*_test.rb"].exclude(LEGACY_SURFACE_TESTS)
-end
-
Rake::TestTask.new("test:unit") do |t|
t.libs << "test"
t.libs << "lib"
- t.test_files = FileList["test/unit/**/*_test.rb", LEGACY_SURFACE_TESTS]
+ t.test_files = FileList["test/unit/**/*_test.rb"]
end
Rake::TestTask.new("test:integration") do |t|
@@ -79,17 +57,7 @@ task "clobber" do
puts "Cleanup tmp/"
FileUtils.rm_rf(Dir["./tmp/*"])
end
-
-task "test:benchmark" do
- require_relative "scripts/benchmark/find_region_benchmark"
- benchmark = Capybara::Screenshot::Diff::Drivers::FindRegionBenchmark.new
-
- puts "For Medium Screen Size: 800x600"
- benchmark.for_medium_size_screens
-
- puts ""
- puts "*" * 100
-
- puts "For Small Screen Size: 80x60"
- benchmark.for_small_images
-end
+# `test:benchmark` is deleted rather than repointed: it required
+# scripts/benchmark/find_region_benchmark, which is not in this repo, so the
+# task raised LoadError on every invocation -- and its body named a v1
+# constant this release removes.
diff --git a/bin/console b/bin/console
index 0c77c72b..85227017 100755
--- a/bin/console
+++ b/bin/console
@@ -2,7 +2,7 @@
# frozen_string_literal: true
require "bundler/setup"
-require "capybara/screenshot/diff"
+require "snap_diff"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
diff --git a/bin/dtest b/bin/dtest
index c5ecef7c..101b1e0e 100755
--- a/bin/dtest
+++ b/bin/dtest
@@ -7,7 +7,7 @@ export DOCKER_DEFAULT_PLATFORM=linux/amd64
# Define allowed environment variables to pass to Docker
ALLOWED_ENV_VARS=(
"CI" "DEBUG" "TEST_ENV" "RAILS_ENV" "RACK_ENV" "COVERAGE" "DISABLE_ROLLBACK_COMPARISON_RUNTIME_FILES"
- "RECORD_SCREENSHOTS" "TEST" "TESTOPTS" "SCREENSHOT_DRIVER"
+ "RECORD_SCREENSHOTS" "TEST" "TESTOPTS"
)
# Build the Docker env args string
diff --git a/capybara-screenshot-diff.gemspec b/capybara-screenshot-diff.gemspec
index fea3dbc2..495c5b4c 100644
--- a/capybara-screenshot-diff.gemspec
+++ b/capybara-screenshot-diff.gemspec
@@ -28,4 +28,13 @@ Gem::Specification.new do |spec|
spec.add_development_dependency "actionpack", ">= 7.1", "< 9"
spec.add_development_dependency "activesupport", ">= 7.1", "< 9"
spec.add_runtime_dependency "capybara", ">= 2", "< 4"
+ # 2.1 removed the driver abstraction: libvips is the only backend, so the
+ # gem that binds it is a hard dependency rather than something the user is
+ # told to add. Without this an install resolves fine and then dies at the
+ # first comparison -- a resolver error is the better failure.
+ #
+ # ruby-vips 2.x is the current major line; the gem itself needs system
+ # libvips >= 8.2, which no gemspec constraint can express -- see
+ # docs/drivers.md for the system package.
+ spec.add_runtime_dependency "ruby-vips", ">= 2.0", "< 3"
end
diff --git a/docs/UPGRADING.md b/docs/UPGRADING.md
index e7b623e0..9b758491 100644
--- a/docs/UPGRADING.md
+++ b/docs/UPGRADING.md
@@ -1,6 +1,258 @@
# Upgrading
-## Upgrading to v2.0 (alpha)
+## Upgrading to v2.1
+
+**2.0 was the transitional release: both APIs worked, and everything that was going to die warned
+about it. 2.1 is the cleanup — it removes all of it, in one release. There is no 3.0.**
+
+Nothing here is deprecated. The v1 *implementation* is gone; touching what it held is a
+`NameError` or a `NoMethodError`, not a warning.
+
+**Your entry point and your settings are kept on purpose, permanently.** We went looking for who
+actually uses this gem before finalising the deletion, and the answer changed the plan:
+
+1. **`CapybaraScreenshotDiff` and `Capybara::Screenshot::Diff` still resolve.** They are eager
+ aliases of `SnapDiff` — the same module object, so `CapybaraScreenshotDiff::DSL` *is*
+ `SnapDiff::DSL`, `const_defined?` and `defined?` keep answering, and `rescue
+ CapybaraScreenshotDiff::CapybaraScreenshotDiffError` still catches. The v1 `require` paths
+ (`capybara_screenshot_diff/minitest`, `.../rspec`, `.../cucumber`, `.../dsl`,
+ `capybara/screenshot/diff`) still load, as one-line entries.
+2. **Every setting still answers on the old holders.** `Capybara::Screenshot::Diff.tolerance =`,
+ `Capybara::Screenshot.window_size =`, `include Capybara::Screenshot::Diff`,
+ `Capybara::Screenshot::Diff.configure { |screenshot, diff| … }` — all of it works, delegating
+ to the one storage in `SnapDiff.config`. Aliasing the names you *import* without the names you
+ *call* would be worse than aliasing neither: the constant resolves, you think you are fine,
+ and line 2 of your test helper explodes.
+3. **`driver` and `shift_distance_limit` raise instead of vanishing.** A removed setter that
+ simply disappears is the worst kind of removal — code guarded by `respond_to?` keeps running
+ with the setting silently doing nothing. Both now raise `ArgumentError` with a message naming
+ the replacement (except `driver = :vips`/`:auto`, which are accepted and ignored — see
+ [Image processing](#2-image-processing)).
+
+What is genuinely gone is the machinery: the driver abstraction, the chunky_png backend, the
+deprecation channel, `SnapDiff.start`, `SnapDiff::Drivers.loaded`/`.available`, and the v1
+implementation trees under `lib/capybara*`. Roughly 3,700 lines.
+
+You should still migrate to `SnapDiff::*` — that is the name the docs, the errors and every
+future release use — but nothing about your suite has to move in the same commit as the upgrade.
+
+**Estimated upgrade time:** 15 minutes. A real consumer (a Jekyll/Rails site with committed
+baselines, running its suite in Docker) was upgraded end to end against this release: **17 lines
+across two files, zero blockers** — 38 runs, 0 failures, 55 screenshots compared, byte-identical
+before and after. Every v1 API had a canonical equivalent.
+
+> **Upgrading from 1.x?** Do it in two steps. Go to 2.0 first, run your suite, fix what the
+> deprecation warnings point at, *then* come here. The [2.0 section](#upgrading-to-v20-from-v1x)
+> below is still the guide for that first step.
+
+### 1. Namespaces
+
+#### Requires
+
+| Before | After |
+|---|---|
+| `require "capybara_screenshot_diff/minitest"` | `require "snap_diff/integrations/minitest"` |
+| `require "capybara_screenshot_diff/rspec"` | `require "snap_diff/integrations/rspec"` |
+| `require "capybara_screenshot_diff/cucumber"` | `require "snap_diff/integrations/cucumber"` |
+| `require "capybara_screenshot_diff/reporters/html"` | `require "snap_diff/reporters/html"` |
+| `require "capybara_screenshot_diff/static"` | `require "snap_diff/static"` |
+| `require "capybara/screenshot/diff"` | `require "snap_diff"` |
+
+Note the `integrations/` segment: `require "snap_diff/minitest"` is a `LoadError`.
+
+Every "Before" line above still works — they are kept as one-line entries that require the
+canonical path for you. The exceptions are `capybara_screenshot_diff/reporters/html` and
+`capybara_screenshot_diff/static`, which are gone: nothing was found using them, and unlike the
+integration entries they were never the line a test helper opens with.
+
+#### Gemfile
+
+The gem is published under two names with identical content and versions. Both still install and
+both auto-require.
+
+```ruby
+gem "snap_diff-capybara" # the forward-looking name
+gem "capybara-screenshot-diff" # equally fine — same content, same version
+```
+
+Pick **one**. Having both in a bundle activates two gems shipping identical `lib/` paths, so every
+`require` resolves from whichever activated first and the two versions can drift apart invisibly —
+the lockfile shows nothing wrong. 2.1 refuses that setup at load with a
+`SnapDiff::DualInstallError` naming both gems, rather than letting it run.
+
+#### Configuration
+
+Both v1 holders collapsed into one object. `SnapDiff.configure` is the single entry point.
+
+| Before | After |
+|---|---|
+| `Capybara::Screenshot. = …` | `SnapDiff.config. = …` |
+| `Capybara::Screenshot::Diff. = …` | `SnapDiff.config. = …` |
+| `Capybara::Screenshot::Diff.configure { \|screenshot, diff\| … }` | `SnapDiff.configure { \|config\| … }` |
+| `SnapDiff.start { \|screenshot, diff\| … }` | `SnapDiff.configure { \|config\| … }` |
+
+The setting names are unchanged; only the receiver moves. The one rename:
+`Capybara::Screenshot.enabled` is `SnapDiff.config.screenshot_enabled`, because
+`SnapDiff.config.enabled` is the old `Capybara::Screenshot::Diff.enabled`. They are two genuinely
+different settings feeding different branches of `active?`, and 2.1 does not collapse them —
+`Capybara::Screenshot.enabled` and `Capybara::Screenshot::Diff.enabled` keep meaning what they
+always meant. All settings are listed in the [Configuration Reference](configuration.md).
+
+**Every "Before" row above still works.** The old accessors are generated from the setting list
+itself and delegate to `SnapDiff.config`, so there is one storage and two views — a write through
+either is visible through the other structurally, not by synchronisation. That includes the
+instance-level form: `include Capybara::Screenshot::Diff` still brings the settings in as instance
+methods, and `Capybara::Screenshot::Diff.configure { |screenshot, diff| … }` still yields two
+usable holders (both are the one config object now).
+
+`SnapDiff.start` is the one config entry point that could **not** survive: it yielded the two v1
+*modules*, not settings, so there was nothing left for it to hand you. Use
+`Capybara::Screenshot::Diff.configure` (unchanged) or `SnapDiff.configure`. Same for
+`SnapDiff.silence_deprecations` and `SNAP_DIFF_SILENCE_DEPRECATIONS`: with no deprecations left to
+emit, there is nothing to silence.
+
+#### Constants and includes
+
+| Before | After |
+|---|---|
+| `Capybara::Screenshot::Os.name` | `SnapDiff::Os.name` |
+| `Capybara::Screenshot::Diff::ImageCompare` | `SnapDiff::Comparison` |
+| `Capybara::Screenshot::Diff::Difference` | `SnapDiff::ComparisonResult` |
+| `CapybaraScreenshotDiff::Reporters::HTML` | `SnapDiff::Reporters::HTML` |
+| `CapybaraScreenshotDiff::SnapManager` / `::Snap` | `SnapDiff::SnapManager` / `SnapDiff::Snap` |
+| `CapybaraScreenshotDiff.serve` | `SnapDiff.serve` |
+| `CapybaraScreenshotDiff.reporters <<` | `SnapDiff::Reporting.register` |
+| `CapybaraScreenshotDiff.finalize_reporters!` | `SnapDiff::Reporting.finalize!` |
+| `include CapybaraScreenshotDiff::DSL` `include CapybaraScreenshotDiff::Minitest::Assertions` | `include SnapDiff::Minitest::Assertions` — **the two collapse into one**. Both old spellings keep working: they are aliases of the same modules |
+| `rescue CapybaraScreenshotDiff::CapybaraScreenshotDiffError` | `rescue SnapDiff::Error` — the old name is kept as an alias. A `NameError` inside a `rescue` clause fires only when an exception is already in flight, which is the worst possible moment to discover it |
+
+**`Capybara::Screenshot::Os` is the one to grep for.** In the real upgrade it was the only hard
+crash. On 2.0 it raises `NameError` from the shim internals once the require line has been
+migrated but the constant has not — a half-migrated setup looks fine (the config setters keep
+working) until `Os` aborts the whole suite before a single test runs. On 2.1 it is simply gone.
+
+**What did not change:** the DSL. `screenshot`, `assert_matches_screenshot`, `capture_screenshot`,
+`assert_no_screenshot_changes`, `screenshot_group`, `screenshot_section` and every per-screenshot
+option work exactly as before. Your baselines are unchanged and do not need re-recording.
+
+### 2. Image processing
+
+**libvips is the only backend, and `ruby-vips` is a gemspec runtime dependency (`>= 2.0, < 3`).**
+You no longer add it yourself; Bundler installs it. libvips itself is still a system package
+(`brew install vips`, `apt-get install libvips`) — see [Image Processing](drivers.md).
+
+Before this, *neither* driver was declared, so a machine without either got a runtime error deep
+in a test run. Now it is a resolver error at `bundle install`.
+
+| Before | After |
+|---|---|
+| `gem "ruby-vips"` in your Gemfile | delete it (harmless to keep) |
+| `SnapDiff.config.driver = :vips` / `= :auto` | accepted and ignored — delete it when convenient |
+| `SnapDiff.config.driver = :chunky_png` | **`ArgumentError`** — install libvips + `ruby-vips` and delete the line |
+| `screenshot "index", driver: :vips` | accepted and ignored; `driver: :chunky_png` raises |
+| `shift_distance_limit` (anywhere) | **`ArgumentError`** — no equivalent, see below |
+| `SnapDiff::Drivers.available` to branch on what is installed | nothing to branch on |
+| `SnapDiff::Drivers.loaded[:mine] = MyDriver` | **no replacement** |
+| `include SnapDiff::Driver` in your own driver | **no replacement** |
+
+#### Nothing here breaks quietly
+
+An earlier draft of 2.1 deleted `driver` and `shift_distance_limit` outright. That turned out to
+be the worst available option: a deleted setter makes `respond_to?`-guarded code evaporate, and a
+deleted per-screenshot option is inert because the options hash is free-form. Either way the
+setting stops applying and nothing says so. So:
+
+- **`driver` is accepted and ignored** — globally and per screenshot — for every value except
+ `:chunky_png`. If you were already asking for `:vips`, you get exactly what you asked for and
+ no noise. `:chunky_png` raises `ArgumentError` naming libvips, `ruby-vips` and this guide,
+ because it is the one value the gem can no longer honour.
+- **`shift_distance_limit` raises `ArgumentError`** — globally and per screenshot, whichever way
+ you set it, including through a `respond_to?` guard. There is no value that quietly does
+ nothing.
+- The per-screenshot options hash is checked at the one place every option passes through, so
+ `screenshot "index", shift_distance_limit: 5` raises the same error as the global setter.
+
+#### `shift_distance_limit` has no replacement
+
+It was implemented **only** by the ChunkyPNG driver, and libvips has no shift-distance
+comparison. Use one of:
+
+| Instead | Why |
+|---|---|
+| `median_filter_window_size` | The same idea and far faster — smooths the image before comparing. Per-screenshot only; there is no global setting for it |
+| `tolerance` | Allows a ratio of pixels to differ, wherever they are |
+| `color_distance_limit` | Allows each pixel to differ by a colour distance |
+
+See [Allowed shift distance](configuration.md#allowed-shift-distance-removed-in-21).
+
+#### Numbers in failure messages change
+
+If you assert on comparison output, note that libvips reports differently from ChunkyPNG:
+`area_size` and `region` come out as floats, and there is no `max_color_distance`. For the gem's
+own `a`/`c` fixtures:
+
+```
+before (ChunkyPNG): ({"area_size":629,"region":[11,3,48,20],"max_color_distance":187.4})
+after (libvips): ({"area_size":684.0,"region":[11.0,3.0,49.0,21.0],"difference_level":0.0653125})
+```
+
+This only surfaced now because a `Comparison` built without an explicit `driver:` defaulted to
+ChunkyPNG. Anyone going through the normal DSL was already on libvips via `driver: :auto`.
+
+#### Custom drivers: there is no migration path
+
+The abstraction is removed whole — the `SnapDiff::Driver` mixin, the `SnapDiff::Drivers` registry
+(`.loaded`, `.available`, `.for`, `.registry`, `.detect_available`), `AVAILABLE_DRIVERS`,
+`SnapDiff::Utils.detect_available_drivers`, and `:auto` selection. **A third-party driver stops
+working on 2.1 and nothing replaces it.** This is a deliberate call, not an oversight: one
+backend is what keeps every option meaning one thing. If you maintain one, say so on the
+[issue tracker](https://github.com/snap-diff/snap_diff-capybara/issues) — that is the only thing
+that can reopen it.
+
+`SnapDiff::Drivers::VipsDriver` survives, and `SnapDiff::Drivers` survives as its namespace — not
+as a registry.
+
+### The whole diff, from the real upgrade
+
+Two files, seventeen lines:
+
+```diff
+ # test/test_helper.rb
+- require "capybara_screenshot_diff/minitest"
+- require "capybara_screenshot_diff/reporters/html"
++ require "snap_diff/integrations/minitest"
++ require "snap_diff/reporters/html"
+
+- Capybara::Screenshot.window_size = [1400, 1400]
+- Capybara::Screenshot::Diff.tolerance = 0.001
++ SnapDiff.config.window_size = [1400, 1400]
++ SnapDiff.config.tolerance = 0.001
+
+- Capybara::Screenshot::Os.name
++ SnapDiff::Os.name
+
+ # test/application_system_test_case.rb
+- include CapybaraScreenshotDiff::DSL
+- include CapybaraScreenshotDiff::Minitest::Assertions
++ include SnapDiff::Minitest::Assertions
+```
+
+### Checklist
+
+- [ ] `grep -rn 'capybara_screenshot_diff\|capybara/screenshot/diff' test/ spec/ features/` — the require lines
+- [ ] `grep -rn 'Capybara::Screenshot\|CapybaraScreenshotDiff' test/ spec/ features/ config/` — constants and settings. These keep working, so this one is a rename-at-leisure list, not a blocker. The exception is `Capybara::Screenshot::Os`, which is genuinely gone
+- [ ] `grep -rn 'driver:\|shift_distance_limit\|silence_deprecations\|SnapDiff.start' .` — the driver-era settings. `shift_distance_limit` and `driver: :chunky_png` now raise rather than going quiet, so the suite will find them for you if you skip this step
+- [ ] Remove `gem "ruby-vips"` and any `chunky_png` / `oily_png` lines from your Gemfile
+- [ ] `bundle install`, then run the suite — baselines do not need re-recording
+
+---
+
+## Upgrading to v2.0 (from v1.x)
+
+> **History.** This section describes **2.0**, the transitional release, where the v1 names still
+> worked and warned. On 2.1 the machinery behind them is gone — only the two entry namespaces
+> survive, as aliases; see [Upgrading to v2.1](#upgrading-to-v21) above. Keep reading only if you
+> are stepping from 1.x through 2.0.
### Overview
@@ -210,7 +462,7 @@ warns once per process per subject, through the same channel and the same silenc
|---|---|---|
| select the ChunkyPNG driver — `driver: :chunky_png`, `SnapDiff.config.driver = :chunky_png`, or the legacy `Capybara::Screenshot::Diff.driver =` | the `:chunky_png` driver | add `gem "ruby-vips"` (plus the libvips system package) and drop the option |
| run on `driver: :auto` **without `ruby-vips` installed** | the `:auto` fallback to ChunkyPNG | same — install libvips + `ruby-vips`. This is the case worth reading twice: nothing in your setup says `chunky_png`, so the warning is the only sign that 2.1 will break this process |
-| set `shift_distance_limit` — globally or per screenshot | `shift_distance_limit` (ChunkyPNG-only) | `median_filter_window_size`, `tolerance`, or `color_distance_limit` — see [Configuration](configuration.md#allowed-shift-distance) |
+| set `shift_distance_limit` — globally or per screenshot | `shift_distance_limit` (ChunkyPNG-only) | `median_filter_window_size`, `tolerance`, or `color_distance_limit` — see [Configuration](configuration.md#allowed-shift-distance-removed-in-21) |
| read `SnapDiff::Drivers.loaded` (the custom-driver registry) | the registry | nothing — custom drivers are removed, see below |
| read `SnapDiff::Drivers.available` | driver detection | require `ruby-vips` instead of branching on a detected list |
| `include SnapDiff::Driver` in your own driver class | the driver mixin | nothing — see below |
@@ -793,7 +1045,7 @@ All screenshot baselines are compatible — no data loss.
## Need Help?
- **Documentation:** [README.md](../README.md)
-- **Changelog:** [CHANGELOG.md](CHANGELOG.md)
+- **Changelog:** [CHANGELOG.md](../CHANGELOG.md)
- **Issues:** [GitHub Issues](https://github.com/snap-diff/snap_diff-capybara/issues)
- **DeepWiki:** [Code Documentation](https://deepwiki.com/snap-diff/snap_diff-capybara)
diff --git a/docs/architecture.md b/docs/architecture.md
index dc864310..29c2c3f1 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -1,8 +1,8 @@
# Architecture
-This document describes the internal architecture of `capybara-screenshot-diff` — how screenshots are captured, compared, and reported, and how the components fit together.
+This document describes the internal architecture of the gem — how screenshots are captured, compared, and reported, and how the components fit together.
-Since the v2 namespace move (ADR-004), the implementation lives in `lib/snap_diff/` under the `SnapDiff` namespace. The old file paths (`lib/capybara/screenshot/diff/`, `lib/capybara_screenshot_diff/`) remain as thin forwarders, and the old constants resolve to the same objects via `lib/snap_diff/legacy_shims.rb` with a one-time deprecation warning. Class names below use the canonical `SnapDiff::` names, with legacy names noted where they differ. For the user-facing view of the same surface, see [SnapDiff — the canonical API](snapdiff.md).
+The implementation lives in `lib/snap_diff/` under the `SnapDiff` namespace, and since 2.1 that is the only namespace: the v1 file trees (`lib/capybara/screenshot/diff/`, `lib/capybara_screenshot_diff/`), the `legacy_shims.rb` that resolved their constants, and the deprecation machinery around them were deleted. 34 files ship under `lib/`, listed in [File Layout](#file-layout) below. Class names below are the canonical ones, with the v1 name noted in passing where the history explains a shape. For the user-facing view of the same surface, see [SnapDiff — the canonical API](snapdiff.md).
## Overview
@@ -42,11 +42,10 @@ Since the v2 namespace move (ADR-004), the implementation lives in `lib/snap_dif
│
▼
┌──────────────────────────────────────────┐
-│ Drivers (image processing backends) │
-│ ┌──────────┐ ┌──────────────────────┐ │
-│ │ Vips │ │ ChunkyPNG │ │
-│ │ (fast) │ │ (no native deps) │ │
-│ └──────────┘ └──────────────────────┘ │
+│ VipsDriver (the image backend) │
+│ ┌────────────────────────────────────┐ │
+│ │ libvips via ruby-vips │ │
+│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
```
@@ -54,8 +53,6 @@ Since the v2 namespace move (ADR-004), the implementation lives in `lib/snap_dif
### 1. DSL Layer (`lib/snap_diff/dsl.rb`)
-`SnapDiff::DSL` — `CapybaraScreenshotDiff::DSL` remains an eager same-object alias.
-
The entry point for test code. `assert_matches_screenshot` is the primary assertion method (captures and compares). `screenshot` is a convenience wrapper with a `compare:` option — `compare: true` (default) delegates to `assert_matches_screenshot`, while `compare: false` delegates to the new `capture_screenshot` method. `capture_screenshot` takes screenshots without assertions. `assert_no_screenshot_changes` keeps its behavior but now delegates to `assert_matches_screenshot` (that redirect is part of the #191 fix). Users can safely override `screenshot` in their test classes without affecting internal gem flow.
**Flow:**
@@ -92,7 +89,7 @@ The orchestrator that coordinates capture and comparison:
### 4. Comparison (`lib/snap_diff/comparison.rb`)
-`SnapDiff::Comparison` (legacy name: `ImageCompare`); its result value object is `SnapDiff::ComparisonResult` (`lib/snap_diff/comparison_result.rb`, legacy name: `Difference`).
+`SnapDiff::Comparison` (v1 name: `ImageCompare`); its result value object is `SnapDiff::ComparisonResult` (`lib/snap_diff/comparison_result.rb`, v1 name: `Difference`).
The comparison engine uses a **layered optimization strategy** to balance speed and accuracy:
@@ -108,41 +105,35 @@ The comparison engine uses a **layered optimization strategy** to balance speed
- `processed` guarantees the comparison is complete and returns the result with all metadata
- `Comparison#analyze_difference` handles the actual pixel analysis, delegating to the driver
-### 5. Drivers (`lib/snap_diff/drivers/`)
+### 5. The image backend (`lib/snap_diff/drivers/vips_driver.rb`)
-Drivers abstract image processing operations. Shared default behavior lives in the `SnapDiff::Driver` mixin (`lib/snap_diff/driver.rb`) — it replaced the old `Drivers::BaseDriver` superclass, so concrete drivers `include SnapDiff::Driver` instead of inheriting. Each driver implements:
+`SnapDiff::Drivers::VipsDriver` does the image work. 2.1 removed the abstraction that used to sit around it — the `SnapDiff::Driver` mixin, the `SnapDiff::Drivers` registry (`.loaded` / `.available` / `.for` / `.detect_available`), the `driver:` setting and `driver: :auto`. `ruby-vips` is a gemspec runtime dependency, so there is nothing to detect and nothing to select; `Comparison` and `Screenshoter` each construct a `VipsDriver` directly (it is stateless). `Drivers` survives only as the namespace the class is published under.
-| Operation | VipsDriver | ChunkyPNGDriver |
-|-----------|-----------|-----------------|
-| `load_images` | Vips::Image from file | ChunkyPNG::Image from blob |
-| `same_dimension?` | Compare width × height | Same |
-| `same_pixels?` | Pixel-level equality | Same |
-| `find_difference_region` | Difference mask → Region | Row-by-row scan → Region |
-| `crop` | Vips image crop | ChunkyPNG crop |
-| `save_image_to` | Vips write_to_file | PNG save |
-| `filter_image_with_median` | Vips median filter | Not supported |
-| `add_black_box` | Draw filled rect | No-op (handled differently) |
-| `merge` | Composite images | Not applicable |
-| `highlight_mask` | Conditional color overlay | Not applicable |
+| Operation | VipsDriver |
+|-----------|-----------|
+| `load_images` | `Vips::Image` from file |
+| `same_dimension?` | Compare width × height |
+| `same_pixels?` | Pixel-level equality |
+| `find_difference_region` | Difference mask → Region |
+| `crop` | Vips image crop |
+| `save_image_to` | Vips `write_to_file` |
+| `filter_image_with_median` | Vips median filter |
+| `add_black_box` | Draw filled rect |
+| `merge` | Composite images |
+| `highlight_mask` | Conditional color overlay |
-**Auto-detection:** `SnapDiff::Drivers.detect_available` tries to load `:vips` first (via `ruby-vips` gem), then `:chunky_png`. The `:auto` driver mode picks the first available. `Utils.detect_available_drivers` is the older name and one-lines into it.
+**Loader cache:** `#from_file` passes `revalidate: true`. libvips caches loader operations on filename + mtime, and mtime has one-second resolution — without this, rewriting a screenshot path and re-reading it within the same second serves the PREVIOUS image. See the regression test in `test/unit/drivers/vips_driver_test.rb`.
-**Registry (ADR-008 step 5b):** `SnapDiff::Drivers.loaded` is the canonical driver-class cache — a `name => class` hash filled lazily by `Utils.find_driver_class_for`, and the registration point for custom drivers (the legacy `Capybara::Screenshot::Diff::LOADED_DRIVERS` is an eager same-object alias, so registrations through either land in the same hash). `SnapDiff::Drivers.available` is the canonical read API for the detected list, and since the 3.0-readiness pass the value lives with it, as `SnapDiff::Drivers::AVAILABLE_DRIVERS` — that constant is now the published stubbing point, and the legacy `Capybara::Screenshot::Diff::AVAILABLE_DRIVERS` is an eager same-object alias of it. `SnapDiff::Drivers.for` resolves an options hash to a driver instance. See [Custom drivers](snapdiff.md#custom-drivers).
+There is no custom-driver path; see [SnapDiff — the canonical API](snapdiff.md) and [Image Processing](drivers.md).
### 6. Difference Region Detection
-**VipsDriver** uses a **difference mask** approach:
+`VipsDriver` uses a **difference mask** approach:
1. Compute absolute difference between images: `(new - base).abs`
2. Optional: apply perceptual color distance (CIE dE00) instead of raw RGB
3. Project the mask to find the bounding region of non-zero pixels
4. Return the tight bounding box of all differences
-**ChunkyPNGDriver** uses **row-by-row scanning**:
-1. Scan top-to-bottom, left-to-right for first differing pixel
-2. Expand left/right boundaries within each differing row
-3. Extend bottom boundary to cover all differing rows
-4. Supports shift detection (expensive neighbor pixel search)
-
### 7. SnapManager & Snap (`lib/snap_diff/snap_manager.rb`, `lib/snap_diff/snap.rb`)
**Snap** represents a single screenshot file with path management:
@@ -175,7 +166,7 @@ Handles baseline retrieval from git. Uses `git show HEAD:` to extract the
- Keyboard navigation and shortcuts
- Responsive layout for mobile
-**Custom reporters:** Implement `record(assertions)`, `finalize` and `summary`, then register via `SnapDiff::Reporting.register(reporter)` — the canonical way in, because the append happens under the mutex. The process-global reporter lifecycle (registration, notification, finalization) is owned by `SnapDiff::Reporting` (`lib/snap_diff/reporting.rb`); `CapybaraScreenshotDiff.reporters` / `.finalize_reporters!` are thin public shims over it, and `reporters` stays a mutable array for compatibility (appending directly still works, it just skips the lock). See [Custom reporters](snapdiff.md#custom-reporters).
+**Custom reporters:** Implement `record(assertions)`, `finalize` and `summary`, then register via `SnapDiff::Reporting.register(reporter)` — the way in, because the append happens under the mutex. The process-global reporter lifecycle (registration, notification, finalization) is owned by `SnapDiff::Reporting` (`lib/snap_diff/reporting.rb`); `SnapDiff::Reporting.reporters` stays a mutable array (appending directly still works, it just skips the lock). See [Custom reporters](snapdiff.md#custom-reporters).
### 10. Assertion Lifecycle
@@ -221,57 +212,50 @@ Test begins
Since ADR-008 step 1 the storage ownership is inverted from the original v2 consolidation: **`SnapDiff::Config` (`lib/snap_diff/config.rb`) IS the storage** — one eagerly-created instance, reachable as `SnapDiff.config`, holding every setting as a plain `attr_accessor`. It is the leaf of the config require graph and requires nothing that leads back to either entry point.
-The legacy `Capybara::Screenshot.*` / `Capybara::Screenshot::Diff.*` accessors are thin delegators generated from `SnapDiff::LegacyShims::CONFIG_MAPPING` (both singleton and instance methods, matching what `mattr_accessor` used to define) that forward to that one object. One storage, two views — a write through either surface is visible through the other structurally, not by synchronization.
-
-Since the 3.0-readiness pass, `lib/snap_diff/legacy_shims.rb` is the single file that holds the v1 surface as code: the `const_missing` forwarders, `CONFIG_MAPPING` and its generator, the derived forwarders (`Screenshot.active?`, `Diff.configure`, `Diff.default_options`, …) and `SnapDiff.start`. `Config` itself names nothing from the v1 namespaces — it declares its settings in `Config::SETTINGS`, and `LegacyShims::CONFIG_MAPPING` says which legacy holder each one is exposed on (an invariant pinned by `snap_diff_config_test.rb`). `lib/capybara/screenshot/diff/config_legacy.rb` remains at the old path as a pair of requires.
+2.1 removed the second view. `lib/snap_diff/legacy_shims.rb` — the single file that held the v1 surface as code (the `const_missing` forwarders, `CONFIG_MAPPING` and its delegator generator, the derived forwarders, `SnapDiff.start`) — is deleted, along with `lib/capybara/screenshot/diff/config_legacy.rb`. `Config` declares its settings in `Config::SETTINGS` and nothing maps them onto anything else.
-The two legacy views are organized into two namespaces:
+All 25 are flat on one object; `Config::SETTINGS` still declares them in two informal groups, capture first and comparison second:
-**`Capybara::Screenshot`** — capture settings:
-- `window_size`, `stability_time_limit`, `blur_active_element`, `hide_caret`, `disable_animations`
-- `save_path`, `root`, `screenshot_format`, `add_driver_path`, `add_os_path`
-- `enabled`, `capybara_screenshot_options`
+**Capture (13):** `add_driver_path`, `add_os_path`, `blur_active_element`, `screenshot_enabled`, `hide_caret`, `disable_animations`, `root`, `stability_time_limit`, `window_size`, `save_path`, `use_lfs`, `screenshot_format`, `capybara_screenshot_options`
-**`Capybara::Screenshot::Diff`** — comparison settings:
-- `driver`, `tolerance`, `color_distance_limit`, `perceptual_threshold`, `shift_distance_limit`
-- `area_size_limit`, `skip_area`, `fail_if_new`, `fail_on_difference`, `delayed`
+**Comparison (12):** `delayed`, `area_size_limit`, `fail_if_new`, `pending_if_new`, `fail_on_difference`, `color_distance_limit`, `enabled`, `skip_area`, `tolerance`, `perceptual_threshold`, `screenshoter`, `manager`
-The canonical way in is `SnapDiff.configure { |config| ... }` (all 27 settings flat on one object). `SnapDiff.start` and `Capybara::Screenshot::Diff.configure` are the two-holder block shape over the same storage — since ADR-008 step 7b, `Diff.configure` forwards to `SnapDiff.start` rather than the other way round.
+`SnapDiff.configure { |config| ... }` is the only block form — the two-holder shape (`SnapDiff.start`, `Capybara::Screenshot::Diff.configure`) went with the holders it yielded. `screenshot_enabled` is the one setting whose name differs from its v1 spelling, because a flat object cannot carry two attributes called `enabled`.
-`Config` also owns the derived values that used to live on the legacy modules: `active?` (ex `Capybara::Screenshot.active?`), `screenshot_area` / `screenshot_area_abs`, and `default_options` (ex `Capybara::Screenshot::Diff.default_options`, the option hash handed to `SnapDiff::Comparison`). The legacy module methods one-line forward here.
+`Config` also owns the derived values that used to live on the v1 modules: `active?` (ex `Capybara::Screenshot.active?`, which reads both `enabled` flags), `screenshot_area` / `screenshot_area_abs`, and `default_options` (ex `Capybara::Screenshot::Diff.default_options`, the option hash handed to `SnapDiff::Comparison`).
**Default timing contract:** every default is evaluated once, in `Config#initialize`, which runs at require time of `config.rb` — the same load moment the old `mattr_accessor` default blocks evaluated at. `fail_if_new` (from `ENV["CI"]`) and `root` (from `Rails.root`) must never become lazy read-time defaults. The one deliberately live value is `default_options[:wait]`, a method-body read of `Capybara.default_max_wait_time`.
## File Layout
+All 34 packaged files, one namespace:
+
```
lib/
- snap_diff.rb # SnapDiff module: compare/start/configure/config
- snap_diff/ # Canonical implementation (v2)
+ snap_diff.rb # SnapDiff module: compare/configure/config
+ snap_diff-capybara.rb # Bundler auto-require entry for gem "snap_diff-capybara"
+ snap_diff/
dsl.rb # screenshot(), screenshot_group(), etc.
- config.rb # SnapDiff::Config — THE storage for all 27 settings
+ config.rb # SnapDiff::Config — THE storage for all 25 settings
errors.rb # Error / ExpectationNotMet / UnstableImage / WindowSizeMismatchError
- region.rb # SnapDiff::Region — bounding box (+ eager top-level ::Region alias)
- deprecation.rb # Warn-once-per-constant machinery
- legacy_shims.rb # const_missing forwarders for the old namespaces
+ error_with_filtered_backtrace.rb # Error with filtered stack
+ region.rb # SnapDiff::Region — bounding box
comparison.rb # Layered comparison engine (ex-ImageCompare)
comparison_result.rb # Comparison result value object (ex-Difference)
- driver.rb # SnapDiff::Driver mixin (ex-BaseDriver superclass)
- drivers.rb # Driver factory
drivers/
- vips_driver.rb # VIPS image processing
- chunky_png_driver.rb # ChunkyPNG image processing
+ vips_driver.rb # THE image backend (libvips)
capture/
viewport.rb # Per-capture viewport preparation seam
screenshoter.rb # Basic browser screenshot capture
stable_screenshoter.rb # Stability detection wrapper
screenshot_matcher.rb # Orchestrator for capture + compare
- screenshot_assertion.rb # Assertion + registry objects
+ screenshot_assertion.rb # Assertion + registry objects, session lifecycle
screenshot_namer.rb # Name/path generation with sections/groups
snap_manager.rb # Screenshot file management
snap.rb # Single screenshot file abstraction
reporting.rb # Process-global reporter lifecycle
reporters/
+ default.rb # Annotated diff images + failure message
html.rb # Interactive HTML report reporter
templates/report.html.erb # HTML report template
annotation_service.rb # Diff-image annotation (RED_RGBA / ORANGE_RGBA)
@@ -279,29 +263,16 @@ lib/
area_calculator.rb # Crop/skip area coordinate resolution
browser_helpers.rb # DOM manipulation helpers
attempts_reporter.rb # Debug reporting for unstable captures
- error_with_filtered_backtrace.rb # Error with filtered stack
vcs.rb # Git baseline checkout
- utils.rb # Driver detection
os.rb # OS detection
static.rb # Non-Rails static site serving
- version.rb # Gem version
+ version.rb # Gem version (the gemspec reads it)
integrations/
minitest.rb # Minitest assertions integration
rspec.rb # RSpec matcher integration
cucumber.rb # Cucumber World integration
- capybara_screenshot_diff.rb # Umbrella entry point + eager error-class aliases
- capybara_screenshot_diff/ # Legacy paths — mostly thin forwarders
- minitest.rb / rspec.rb / cucumber.rb # Legacy entry points (load the full gem)
- screenshot_assertion.rb # CapybaraScreenshotDiff session/reporter shims
- ... # Everything else forwards to snap_diff/
- capybara/screenshot/diff.rb # Convenience require (loads minitest)
- capybara/screenshot/diff/
- config_legacy.rb # Legacy accessor surface, delegating to SnapDiff::Config
- region.rb # Forwarder to snap_diff/region.rb
- version.rb # Capybara::Screenshot::Diff::VERSION (gemspec reads it)
- ... # Everything else forwards to snap_diff/
```
-Most legacy `Capybara::Screenshot::Diff::*` and `CapybaraScreenshotDiff::*` constants resolve lazily via `snap_diff/legacy_shims.rb` (`const_missing`), pointing at the same objects with a one-time deprecation warning. The error classes are the deliberate exception: they are **eager** same-object aliases, because `rescue` clauses and `defined?` / `const_defined?` feature detection in adopter code must keep behaving exactly as before (`const_defined?` never triggers `const_missing`). Same for `LOADED_DRIVERS`, pinned as an eager alias of `SnapDiff::Drivers.loaded` so user registrations through the old constant are not silently dropped.
+Gone in 2.1, and worth knowing if you are reading an older commit or an old stack trace: `lib/capybara_screenshot_diff.rb` and its tree, `lib/capybara/screenshot/diff.rb` and its tree, `snap_diff/legacy_shims.rb`, `snap_diff/deprecation.rb`, `snap_diff/removal.rb`, `snap_diff/driver.rb`, `snap_diff/drivers.rb` (the registry — `drivers/` survives as a directory holding one class), `snap_diff/drivers/chunky_png_driver.rb` and `snap_diff/utils.rb` (driver detection).
See [SnapDiff — the canonical API](snapdiff.md) for the canonical surface, and [UPGRADING.md](UPGRADING.md) for the migration guide.
diff --git a/docs/ci-integration.md b/docs/ci-integration.md
index 65d0806f..1878cd02 100644
--- a/docs/ci-integration.md
+++ b/docs/ci-integration.md
@@ -9,15 +9,6 @@ require 'snap_diff/static'
SnapDiff.serve("_site") # or "public", "build", "dist"
```
-
-Legacy names (still supported)
-
-```ruby
-require 'capybara_screenshot_diff/static'
-CapybaraScreenshotDiff.serve("_site")
-```
-
-
This sets up Capybara to serve static files and configures screenshot paths automatically.
## .gitignore Setup
@@ -33,8 +24,7 @@ Only commit the baseline screenshots (e.g., `homepage.png`). The `.base.png`, `.
Add to your test helper:
```ruby
-require 'snap_diff/reporters/html' # canonical
-# require 'capybara_screenshot_diff/reporters/html' # legacy, same thing
+require 'snap_diff/reporters/html'
```
### 2. Reusable composite action (recommended)
diff --git a/docs/configuration.md b/docs/configuration.md
index 2406f5ed..7c91337a 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -2,7 +2,8 @@
## Quick Setup
-**Canonical (v2):** every setting lives on one flat object, `SnapDiff.config`.
+Every setting lives on one flat object, `SnapDiff.config` (a `SnapDiff::Config`), and
+`SnapDiff.configure` is the single way in.
```ruby
# In test_helper.rb or rails_helper.rb
@@ -11,54 +12,40 @@ SnapDiff.configure do |config|
config.stability_time_limit = 1
config.blur_active_element = true
config.hide_caret = true
- config.driver = :vips
config.tolerance = 0.0005
config.color_distance_limit = 15
end
```
-**Legacy (still supported):** the two-holder block, split across `Capybara::Screenshot` and
-`Capybara::Screenshot::Diff`.
-
-```ruby
-Capybara::Screenshot::Diff.configure do |screenshot, diff|
- screenshot.window_size = [1280, 1024]
- screenshot.stability_time_limit = 1
- screenshot.blur_active_element = true
- screenshot.hide_caret = true
- diff.driver = :vips
- diff.tolerance = 0.0005
- diff.color_distance_limit = 15
-end
-```
-
-`SnapDiff::Config` **is** the storage; the legacy accessors are thin delegators onto it. There is
-one source of truth, so a write through either surface is visible through the other — mixing them
-is safe, and you can migrate a suite one line at a time:
+Or set them one at a time:
```ruby
SnapDiff.config.window_size = [1280, 1024]
-Capybara::Screenshot.window_size # => [1280, 1024]
+SnapDiff.config.tolerance # => 0.0005
```
-Every option name below is identical on both surfaces — only the receiver changes. The one
-exception: `Capybara::Screenshot.enabled` is `SnapDiff.config.screenshot_enabled`, because
-`SnapDiff.config.enabled` is taken by `Capybara::Screenshot::Diff.enabled`. See
-[SnapDiff — the canonical API](snapdiff.md) for the full SnapDiff-native surface.
+There are 25 settings, all of them on this object. The v1 two-holder form — the
+`Capybara::Screenshot` / `Capybara::Screenshot::Diff` accessors and the
+`Diff.configure { |screenshot, diff| }` block — was removed in 2.1; see
+[UPGRADING.md](UPGRADING.md#upgrading-to-v21). The option names did not change, only the
+receiver. The one exception: the old `Capybara::Screenshot.enabled` is
+`SnapDiff.config.screenshot_enabled`, because `SnapDiff.config.enabled` is the old
+`Capybara::Screenshot::Diff.enabled`. See
+[SnapDiff — the canonical API](snapdiff.md) for the full object map.
**Note:** `fail_if_new` defaults to `true` in CI environments (when `ENV['CI']` is set). New screenshots are allowed locally but rejected in CI — no configuration needed.
-**Note:** Setting `Capybara::Screenshot.enabled = false` is sufficient to disable all screenshots. There is no need to define no-op modules or monkey-patch the gem.
+**Note:** Setting `SnapDiff.config.screenshot_enabled = false` is sufficient to disable all screenshots. There is no need to define no-op modules or monkey-patch the gem.
## Recommended tolerance values
-| Use Case | VIPS `tolerance` | ChunkyPNG `color_distance_limit` | `stability_time_limit` |
-|----------|-----------------|--------------------------------|----------------------|
+| Use Case | `tolerance` | `color_distance_limit` | `stability_time_limit` |
+|----------|-------------|------------------------|----------------------|
| Animated/complex pages | 0.01 | 30 | 2s |
| Standard Rails apps | 0.001 (default) | 15 | 1s |
| Pixel-perfect design tests | 0.0001 | 5 | 1s |
-**Note:** VIPS defaults to `tolerance: 0.001` (allows 0.1% pixel difference). ChunkyPNG has no default tolerance.
+**Note:** `tolerance` defaults to 0.001 (allows 0.1% pixel difference).
## Choosing the Right Color Comparison Method
@@ -66,20 +53,20 @@ exception: `Capybara::Screenshot.enabled` is `SnapDiff.config.screenshot_enabled
### Step 1: Choose color comparison method (pick ONE)
-| Method | Scale | Driver | Best for |
-|--------|-------|--------|----------|
-| `perceptual_threshold` | 0-100+ (dE00) | VIPS only | Cross-OS/browser font rendering, anti-aliasing |
-| `color_distance_limit` | 0-510 (RGBA Euclidean) | VIPS, ChunkyPNG | Legacy setups, fine-grained RGB control |
+| Method | Scale | Best for |
+|--------|-------|----------|
+| `perceptual_threshold` | 0-100+ (dE00) | Cross-OS/browser font rendering, anti-aliasing |
+| `color_distance_limit` | 0-510 (RGBA Euclidean) | Fine-grained RGB control |
**Recommendation:** Use `perceptual_threshold: 2.0` for most cases. It matches human perception and needs less tuning.
-**⚠️ Color comparison methods are exclusive:** `perceptual_threshold` and `color_distance_limit` cannot both be active — if you set both, `perceptual_threshold` wins and `color_distance_limit` is ignored. However, `tolerance` works with **both** methods and is applied by default for VIPS (0.001). This means even with `perceptual_threshold: 2.0`, the `tolerance: 0.001` default still filters results.
+**⚠️ Color comparison methods are exclusive:** `perceptual_threshold` and `color_distance_limit` cannot both be active — if you set both, `perceptual_threshold` wins and `color_distance_limit` is ignored. However, `tolerance` works with **both** methods and is applied by default (0.001). This means even with `perceptual_threshold: 2.0`, the `tolerance: 0.001` default still filters results.
### Step 2: Set tolerance (optional, independent)
| Setting | What it does | Scale |
|---------|--------------|-------|
-| `tolerance` | Maximum allowed *ratio* of different pixels (VIPS) or diff bounding box (ChunkyPNG) | 0.0-1.0 |
+| `tolerance` | Maximum allowed *ratio* of different pixels | 0.0-1.0 |
**Example:** `tolerance: 0.001` allows 0.1% of the image to differ (e.g., 125 pixels in a 1280×1024 screenshot).
@@ -87,8 +74,6 @@ exception: `Capybara::Screenshot.enabled` is `SnapDiff.config.screenshot_enabled
- `perceptual_threshold` / `color_distance_limit` → **"how different can a pixel be?"**
- `tolerance` → **"how many pixels can differ?"**
-**⚠️ Driver difference:** VIPS counts actual different pixels. ChunkyPNG counts the bounding box area around differences — a single pixel diff creates a box, and the entire box area counts against tolerance. This makes ChunkyPNG stricter with the same tolerance value.
-
### Quick start
```ruby
@@ -98,7 +83,7 @@ screenshot 'dashboard', perceptual_threshold: 2.0
# Allow small noise regions
screenshot 'dashboard', perceptual_threshold: 2.0, tolerance: 0.001
-# Legacy ChunkyPNG setup
+# Raw RGB distance instead of perceptual
screenshot 'dashboard', color_distance_limit: 15
```
@@ -106,7 +91,7 @@ screenshot 'dashboard', color_distance_limit: 15
**Tier 1 — Zero config (works immediately):**
`blur_active_element`, `hide_caret`, and `fail_if_new` (in CI) are enabled by default.
-Just `require 'snap_diff/integrations/minitest'` (legacy: `capybara_screenshot_diff/minitest`) and call `screenshot`.
+Just `require 'snap_diff/integrations/minitest'` and call `screenshot`.
**Tier 2 — Set when tests are flaky:**
@@ -122,10 +107,9 @@ Just `require 'snap_diff/integrations/minitest'` (legacy: `capybara_screenshot_d
| Setting | When to use |
|---------|-------------|
| `perceptual_threshold` | Anti-aliasing false positives across OS/browser versions |
-| `shift_distance_limit` | Content shifts by a few pixels (ChunkyPNG only — **removed in 2.1**) |
| `area_size_limit` | Allow small diff regions below a pixel count |
| `color_distance_limit` | Fine-tune raw RGB channel tolerance |
-| `median_filter_window_size` | Smooth noise before comparison (VIPS only) |
+| `median_filter_window_size` | Smooth noise before comparison (per-screenshot option only) |
---
@@ -136,7 +120,7 @@ Just `require 'snap_diff/integrations/minitest'` (legacy: `capybara_screenshot_d
You can specify the desired screen size using
```ruby
-Capybara::Screenshot.window_size = [1024, 768]
+SnapDiff.config.window_size = [1024, 768]
```
This will force the screen shots to the given size, and skip taking screen shots
@@ -147,13 +131,13 @@ unless the desired window size can be achieved.
If you want to skip taking screen shots, set
```ruby
-Capybara::Screenshot.enabled = false
+SnapDiff.config.screenshot_enabled = false
```
You can of course set this by an environment variable
```ruby
-Capybara::Screenshot.enabled = ENV['TAKE_SCREENSHOTS']
+SnapDiff.config.screenshot_enabled = ENV['TAKE_SCREENSHOTS']
```
### Disabling diff
@@ -161,13 +145,13 @@ Capybara::Screenshot.enabled = ENV['TAKE_SCREENSHOTS']
If you want to skip the assertion for change in the screen shot, set
```ruby
-Capybara::Screenshot::Diff.enabled = false
+SnapDiff.config.enabled = false
```
Using an environment variable
```ruby
-Capybara::Screenshot::Diff.enabled = ENV['COMPARE_SCREENSHOTS']
+SnapDiff.config.enabled = ENV['COMPARE_SCREENSHOTS']
```
### Tolerate screenshot differences
@@ -175,7 +159,7 @@ Capybara::Screenshot::Diff.enabled = ENV['COMPARE_SCREENSHOTS']
To allow screenshot differences, but still fail on functional errors, you can set the following option:
```ruby
-Capybara::Screenshot::Diff.fail_on_difference = false
+SnapDiff.config.fail_on_difference = false
```
It defaults to `true`. This can be useful in continuous integration to a generate a screenshot difference
@@ -186,7 +170,7 @@ report while still reporting functional errors.
To fail the test if a new screenshot is taken, set the following option:
```ruby
-Capybara::Screenshot::Diff.fail_if_new = true
+SnapDiff.config.fail_if_new = true
```
If `fail_if_new` is set to `true`, the test will fail if a new screenshot is taken
@@ -199,10 +183,10 @@ that every screenshot taken by your tests corresponds to an expected state of yo
To mark tests as pending (skipped) if a new screenshot is taken without a baseline, set:
```ruby
-Capybara::Screenshot::Diff.pending_if_new = true
+SnapDiff.config.pending_if_new = true
# Required in CI, because fail_if_new defaults to true there and raises before
# the pending marker is applied.
-Capybara::Screenshot::Diff.fail_if_new = false
+SnapDiff.config.fail_if_new = false
```
If `pending_if_new` is set to `true`, the test will be marked as skipped in teardown
@@ -212,7 +196,7 @@ This option is useful when you want to record new screenshots without blocking C
### Screen shot save path
-By default, `Capybara::Screenshot::Diff` saves screenshots to a
+By default, SnapDiff saves screenshots to a
`doc/screenshots` folder, relative to either `Rails.root` (if you're in Rails),
or your current directory otherwise.
@@ -222,17 +206,17 @@ configuration options that that are relevant.
The most likely one you'll want to modify is ...
```ruby
-Capybara::Screenshot.save_path = "other/path"
+SnapDiff.config.save_path = "other/path"
```
-The `save_path` option is relative to `Capybara::Screenshot.root`.
+The `save_path` option is relative to `SnapDiff.config.root`.
-`Capybara::Screenshot.root` defaults to either `Rails.root` (if you're in
+`SnapDiff.config.root` defaults to either `Rails.root` (if you're in
Rails) or your current directory. You can change it to something entirely
different if necessary, such as when using an alternative web framework.
```ruby
-Capybara::Screenshot.root = Hanami.root
+SnapDiff.config.root = Hanami.root
```
### Screen shot stability
@@ -243,7 +227,7 @@ shot will be taken and compared to the first. This is repeated until two
subsequent screen shots are identical.
```ruby
-Capybara::Screenshot.stability_time_limit = 0.1
+SnapDiff.config.stability_time_limit = 0.1
```
This can be overridden on a single screenshot:
@@ -273,7 +257,7 @@ In Chrome the screenshot includes the blinking input cursor. This can make it i
stable screenshot. To get around this you can set the `hide caret` option:
```ruby
-Capybara::Screenshot.hide_caret = true
+SnapDiff.config.hide_caret = true
```
This will make the cursor (caret) transparent (invisible), so the blinking does not delay the screen shot.
@@ -284,7 +268,7 @@ This will make the cursor (caret) transparent (invisible), so the blinking does
Another way to avoid the cursor blinking is to set the `blur_active_element` option:
```ruby
-Capybara::Screenshot.blur_active_element = true
+SnapDiff.config.blur_active_element = true
```
This will remove the focus from the active element, removing the blinking cursor.
@@ -307,45 +291,25 @@ end
The difference is calculated as the euclidean distance. You can also set this globally:
```ruby
-Capybara::Screenshot::Diff.color_distance_limit = 42
+SnapDiff.config.color_distance_limit = 42
```
-### Allowed shift distance
-
-> **Removed in 2.1.** `shift_distance_limit` is implemented only by the ChunkyPNG driver,
-> and 2.1 removes that driver — libvips becomes the only backend. Setting it anywhere
-> (`SnapDiff.config.shift_distance_limit =`, the legacy
-> `Capybara::Screenshot::Diff.shift_distance_limit =`, or `screenshot 'index',
-> shift_distance_limit: 2`) warns once per process in 2.0. There is no vips equivalent:
-> use `median_filter_window_size` (the faster answer to the same problem — see
-> [Drivers](drivers.md#median-filter-size-vips-only)), `tolerance`, or
-> `color_distance_limit`.
+### Allowed shift distance (removed in 2.1)
-Sometimes you want to allow small movements in the images. For example, jquery-tablesorter
-renders the same table slightly differently sometimes. You can set set the shift distance
-threshold for the comparison using the `shift_distance_limit` option to the `screenshot`
-method:
-
-```ruby
-test 'color threshold' do
- visit '/'
- screenshot 'index', shift_distance_limit: 2
-end
-```
-
-The difference is calculated as maximum distance in either the X or the Y axis.
-You can also set this globally:
-
-```ruby
-Capybara::Screenshot::Diff.shift_distance_limit = 1
-```
+The `shift_distance_limit` option let you tolerate small movements in the image (for example,
+jquery-tablesorter rendering the same table slightly differently each run). It was implemented
+only by the ChunkyPNG driver, and 2.1 removed that driver — libvips is the only backend now,
+and it has no shift-distance comparison.
-**Note:** For each increase in `shift_distance_limit` more pixels are searched for a matching color value, and
-this will impact performance **severely** if a match cannot be found.
+Setting it anywhere is a `NoMethodError` on the config object and an ignored key per
+screenshot. Use one of these instead:
-If `shift_distance_limit` is `nil` shift distance is not measured. If `shift_distance_limit` is set,
-even to `0`, shift distance is measured and reported on image differences.
+| Instead of `shift_distance_limit` | Why |
+|---|---|
+| `median_filter_window_size` | The same idea, far faster — smooths the image before comparing. See [Image Processing](drivers.md#median-filter-size) |
+| `tolerance` | Allows a ratio of the pixels to differ, wherever they are |
+| `color_distance_limit` | Allows each pixel to differ by a colour distance |
### Allowed difference size
@@ -362,7 +326,7 @@ end
The difference is calculated as `width * height`. You can also set this globally:
```ruby
-Capybara::Screenshot::Diff.area_size_limit = 42
+SnapDiff.config.area_size_limit = 42
```
@@ -384,7 +348,7 @@ end
The arguments are `[left, top, right, bottom]` for the area you want to ignore. You can also set this globally:
```ruby
-Capybara::Screenshot::Diff.skip_area = [0, 0, 64, 48]
+SnapDiff.config.skip_area = [0, 0, 64, 48]
```
If you need to ignore multiple areas:
@@ -416,7 +380,7 @@ end
You can specify the format of the screenshots taken by setting the `screenshot_format` option. By default, the format is set to `"png"`. However, you can change this to any format supported by your image processing driver. For example, to set the format to `"webp"`, you can do the following:
```ruby
-Capybara::Screenshot.screenshot_format = "webp"
+SnapDiff.config.screenshot_format = "webp"
```
### Customize Capybara#screenshot options
@@ -425,7 +389,7 @@ Allow to bypass screenshot options to Capybara driver.
```ruby
# To create full page screenshots for Selenium
-Capybara::Screenshot.capybara_screenshot_options[:full_page] = true
+SnapDiff.config.capybara_screenshot_options[:full_page] = true
screenshot('index', median_filter_window_size: 2, capybara_screenshot_options: {full_page: false})
```
diff --git a/docs/docker-testing.md b/docs/docker-testing.md
index 5dd861fd..6d1473d2 100644
--- a/docs/docker-testing.md
+++ b/docs/docker-testing.md
@@ -5,7 +5,7 @@
Screenshot tests depend on exact browser rendering, which varies across OS and browser versions. Use `bin/dtest` to run tests inside Docker for consistent, reproducible results matching CI:
```bash
-bin/dtest # Run all tests with all drivers
+bin/dtest # Run all tests against every Capybara driver
bin/dtest test/integration/ # Run specific test directory
```
diff --git a/docs/drivers.md b/docs/drivers.md
index f4faede0..282d74a4 100644
--- a/docs/drivers.md
+++ b/docs/drivers.md
@@ -1,47 +1,41 @@
-# Image Processing Drivers
+# Image Processing
-> **Canonical equivalents.** Global settings shown here as
-> `Capybara::Screenshot::Diff. = …` are also `SnapDiff.config. = …` — same
-> option names, same storage, either surface works. Writing your own driver? See
-> [Custom drivers](snapdiff.md#custom-drivers) for the `SnapDiff::Driver` mixin and how
-> registration in `SnapDiff::Drivers.loaded` works.
+Comparison runs on [libvips](https://www.libvips.org/) through the
+[`ruby-vips`](https://www.rubydoc.info/gems/ruby-vips/Vips/Image) gem. There is nothing to
+configure and nothing to choose: `ruby-vips` is a runtime dependency of this gem, so Bundler
+installs it for you.
-## Removed in 2.1: everything on this page except VIPS
+**libvips itself is a system library** and is not installed by Bundler. Add it with your
+package manager:
-2.1 makes **libvips the only backend**. 2.0 is the transitional release — all of the
-following still works, and warns once per process naming 2.1. Silence the warnings with
-`SnapDiff.silence_deprecations = true` or `SNAP_DIFF_SILENCE_DEPRECATIONS=1`.
+```sh
+brew install vips # macOS
+apt-get install libvips # Debian/Ubuntu
+```
+
+## Removed in 2.1: the driver abstraction
-| Removed in 2.1 | What to do in 2.0 |
+2.0 shipped two backends and a way to pick between them. 2.1 removed the choice.
+
+| Removed in 2.1 | What to do instead |
|---|---|
-| the `:chunky_png` driver | add `gem "ruby-vips"` to your Gemfile and drop `driver: :chunky_png` |
-| `driver: :auto` (and the `:auto` default) | with one backend there is nothing to choose; install `ruby-vips` and the default just works |
-| `shift_distance_limit` | ChunkyPNG-only. Use `median_filter_window_size`, `tolerance` or `color_distance_limit` — see [Configuration](configuration.md#allowed-shift-distance) |
+| the `:chunky_png` driver | install libvips (above); comparisons run on it automatically |
+| the `driver:` setting and `driver: :auto` | delete the line — there is one backend |
+| `shift_distance_limit` | ChunkyPNG-only, with no libvips equivalent. Use `median_filter_window_size`, `tolerance` or `color_distance_limit` |
| `SnapDiff::Driver` (the custom-driver mixin) | nothing — see below |
| `SnapDiff::Drivers.loaded` (the registry) | nothing — see below |
-| `SnapDiff::Drivers.available` (driver detection) | require `ruby-vips` instead of branching on a detected list |
-
-Three related names on the same chopping block stay **silent**, and deliberately so:
-`SnapDiff::Drivers.for` (the gem calls it for every comparison — warning there would fire on
-setups that nothing in this table affects), `SnapDiff::Drivers.detect_available` /
-`SnapDiff::Utils.detect_available_drivers` (run at load, before any user code), and the legacy
-`Capybara::Screenshot::Diff::LOADED_DRIVERS` / `::AVAILABLE_DRIVERS` constant aliases (plain
-constants, nothing to hook). Reach the same values through `.loaded` / `.available` and you
-will hear about them.
-
-**libvips becomes a hard requirement.** Install it with your system package manager
-(`brew install vips`, `apt-get install libvips`) and add `gem "ruby-vips"`. A 2.1 process
-without it cannot compare images at all.
-
-**Custom drivers: there is no migration path.** The driver abstraction is removed whole —
-the `SnapDiff::Driver` mixin, the `SnapDiff::Drivers.loaded` registry, and driver
-selection by name. Third-party drivers stop working in 2.1 and nothing replaces them
-(the decision was made deliberately: no measurable demand, and one backend is what keeps
-the comparison engine honest). If you maintain one, say so on
-[the issue tracker](https://github.com/snap-diff/snap_diff-capybara/issues) before 2.1
-ships — that is the only thing that can change this.
-
-## Perceptual color comparison (VIPS only)
+| `SnapDiff::Drivers.available` / `SnapDiff::Utils.detect_available_drivers` | nothing to detect; a missing `ruby-vips` is now a Bundler resolution error |
+
+`SnapDiff::Drivers::VipsDriver` is the one name from this area that survives, and it is
+internal: nothing in normal use has to mention it.
+
+**Custom drivers: there is no migration path.** The abstraction is removed whole — the
+mixin, the registry, and selection by name. Third-party drivers stop working and nothing
+replaces them. This was deliberate: one backend is what keeps the comparison engine honest.
+If you maintain one, say so on
+[the issue tracker](https://github.com/snap-diff/snap_diff-capybara/issues).
+
+## Perceptual color comparison
By default, color differences are measured using raw RGB channel distance. This can produce
false positives from anti-aliasing and sub-pixel font rendering — the same page rendered on
@@ -56,7 +50,7 @@ on the dE00 scale and are automatically ignored.
screenshot 'dashboard', perceptual_threshold: 2.0
# Global: apply to all screenshots
-Capybara::Screenshot::Diff.perceptual_threshold = 2.0
+SnapDiff.config.perceptual_threshold = 2.0
# dE00 scale reference:
# < 1.0 — not perceptible by human eyes
@@ -75,42 +69,13 @@ These options use different scales and algorithms:
- `perceptual_threshold` → CIE dE00 perceptual distance (0-100+)
- `color_distance_limit` → Euclidean RGBA distance (0-510)
-**Choose one based on your driver setup:**
-- VIPS with `ruby-vips` gem → prefer `perceptual_threshold`
-- ChunkyPNG (no native dependencies) → use `color_distance_limit`
-
-## Available Image Processing Drivers
-
-There are several image processing supported by this gem.
-There are several options to setup active driver: `:auto`, `:chunky_png` and `:vips`.
-
-* `:auto` - will try to load `:vips` if there is gem `ruby-vips`, in other cases will load `:chunky_png`
-* `:chunky_png` and `:vips` will load correspondent driver
-
-> **2.1 keeps only `:vips`.** `:auto` and `:chunky_png` are removed; each warns once per
-> process in 2.0. If `:auto` is quietly running you on ChunkyPNG today (no `ruby-vips`
-> installed), the warning says so — that is the setup 2.1 breaks.
-
-## Enable VIPS image processing
-
-[Vips](https://www.rubydoc.info/gems/ruby-vips/Vips/Image) driver provides a faster comparison,
-and could be enabled by adding `ruby-vips` to `Gemfile`.
-
-If need to setup explicitly Vips driver, there are several ways to do this:
-
-* Globally: `Capybara::Screenshot::Diff.driver = :vips`
-* Per screenshot option: `screenshot 'index', driver: :vips`
-
-With enabled VIPS there are new alternatives to process differences, which are easier to find and support.
-For example, `shift_distance_limit` is a very heavy operation. Instead, use `median_filter_window_size`.
-
-## Tolerance level (vips only)
+## Tolerance level
You can set a "tolerance" anywhere from 0% to 100%. This is the amount of change that's allowable.
If the screenshot has changed by more than that amount, it'll flag it as a failure.
-This is alternative to "Allowed difference size", only the difference that area calculates including valid pixels.
-But "tolerance" compares only different pixels.
+This is an alternative to "Allowed difference size", where the difference area is calculated
+including valid pixels. "Tolerance" compares only different pixels.
You can use the `tolerance` option to the `screenshot` method to set level:
@@ -125,11 +90,11 @@ end
You can also set this globally:
```ruby
-# Default for VIPS is 0.001 (0.1% pixel difference allowed)
-Capybara::Screenshot::Diff.tolerance = 0.001
+# Default is 0.001 (0.1% pixel difference allowed)
+SnapDiff.config.tolerance = 0.001
```
-## Median filter size (vips only)
+## Median filter size
This is an alternative to "Allowed shift distance", but much faster.
You can find more about this strategy on [Median Filter](https://en.wikipedia.org/wiki/Median_filter).
diff --git a/docs/framework-setup.md b/docs/framework-setup.md
index 392f1f8b..cdc6d033 100644
--- a/docs/framework-setup.md
+++ b/docs/framework-setup.md
@@ -1,38 +1,23 @@
# Framework Setup
-> **Canonical equivalents.** This page uses the legacy `CapybaraScreenshotDiff` names, which keep
-> working. Each has a `SnapDiff` home:
->
-> | This page | Canonical |
-> |-----------|-----------|
-> | `require "capybara_screenshot_diff/minitest"` | `require "snap_diff/integrations/minitest"` |
-> | `require "capybara_screenshot_diff/rspec"` | `require "snap_diff/integrations/rspec"` |
-> | `require "capybara_screenshot_diff/cucumber"` | `require "snap_diff/integrations/cucumber"` |
-> | `CapybaraScreenshotDiff::DSL` | `SnapDiff::DSL` |
-> | `CapybaraScreenshotDiff::Minitest::Assertions` | `SnapDiff::Minitest::Assertions` |
-> | `CapybaraScreenshotDiff.finalize_reporters!` | `SnapDiff::Reporting.finalize!` |
->
-> The canonical setup, written out in full, is in [SnapDiff — the canonical API](snapdiff.md).
-
-## Including DSL
-
-To use the screenshot capturing and change detection features in your tests, include the `CapybaraScreenshotDiff::DSL` in your test classes. It provides the `screenshot` method to capture and compare screenshots.
-
-There are different modules for different testing frameworks integrations.
+Minitest, RSpec and Cucumber are supported out of the box. Each is one require plus, in some
+cases, one include.
+
+> The require path is `snap_diff/integrations/…`, not `snap_diff/…` —
+> `require "snap_diff/minitest"` raises `LoadError`.
## Minitest
-For Minitest, need to require `capybara_screenshot_diff/minitest`.
-In your test class, include the `CapybaraScreenshotDiff::Minitest::Assertions` module:
+Require `snap_diff/integrations/minitest`, then include `SnapDiff::Minitest::Assertions` in your
+test class. It brings `SnapDiff::DSL` with it, so a separate `include SnapDiff::DSL` is not
+needed.
```ruby
-require 'capybara_screenshot_diff/minitest'
+require 'snap_diff/integrations/minitest'
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
- # Make the Capybara & Capybara Screenshot Diff DSLs available in tests
- include CapybaraScreenshotDiff::DSL
- # Make `assert_*` methods behave like Minitest assertions
- include CapybaraScreenshotDiff::Minitest::Assertions
+ # `screenshot` / `assert_matches_screenshot`, wired to Minitest's assertion counter
+ include SnapDiff::Minitest::Assertions
def test_my_feature
visit '/'
@@ -43,16 +28,11 @@ end
## RSpec
-To use the screenshot capturing and change detection features in your tests,
-include the `CapybaraScreenshotDiff::DSL` in your test classes.
-It adds `match_screenshot` matcher to RSpec.
-
-> **Important**:
-> The `CapybaraScreenshotDiff::DSL` is automatically included in all feature and system tests by default.
-
+Requiring `snap_diff/integrations/rspec` registers the `match_screenshot` matcher and includes
+`SnapDiff::DSL` into `type: :feature` and `type: :system` examples automatically.
```ruby
-require 'capybara_screenshot_diff/rspec'
+require 'snap_diff/integrations/rspec'
describe 'Permissions admin', type: :feature do
it 'works with permissions' do
@@ -60,10 +40,13 @@ describe 'Permissions admin', type: :feature do
expect(page).to match_screenshot('home_page')
end
end
+```
+For other example types, include the DSL yourself:
+```ruby
describe 'Permissions admin', type: :non_feature do
- include CapybaraScreenshotDiff::DSL
+ include SnapDiff::DSL
it 'works with permissions' do
visit('/')
@@ -74,13 +57,14 @@ end
## Cucumber
-Load Cucumber support by adding the following line (typically to your `features/support/env.rb` file):
+Load Cucumber support by adding the following line (typically to your `features/support/env.rb`
+file):
```ruby
-require 'capybara_screenshot_diff/cucumber'
+require 'snap_diff/integrations/cucumber'
```
-And in the steps you can use:
+The DSL is added to the Cucumber `World`, so steps can call it directly:
```ruby
Then('I should not see any visual difference') do
@@ -88,14 +72,15 @@ Then('I should not see any visual difference') do
end
```
+This file must be loaded from inside a Cucumber run — it calls `World`, `Before`, `After` and
+`AfterAll` at load time and raises `NoMethodError` if required outside one.
+
## Custom Test Frameworks
-Minitest, RSpec, and Cucumber are supported out of the box. For other frameworks, call the
-end-of-suite hook yourself:
+For other frameworks, call the end-of-suite hook yourself:
```ruby
-SnapDiff::Reporting.finalize! # canonical
-CapybaraScreenshotDiff.finalize_reporters! # legacy, same thing
+SnapDiff::Reporting.finalize!
```
This generates the HTML report and prints the summary. A framework also needs the per-test
diff --git a/docs/migration-guide.md b/docs/migration-guide.md
index 2859aedc..6f5d84ac 100644
--- a/docs/migration-guide.md
+++ b/docs/migration-guide.md
@@ -36,14 +36,14 @@ end
**After (capybara-screenshot-diff):**
```ruby
# Gemfile
-gem 'capybara-screenshot-diff'
+gem 'snap_diff-capybara'
# test helper
-require 'capybara_screenshot_diff/minitest'
+require 'snap_diff/integrations/minitest'
# test class
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
- include CapybaraScreenshotDiff::Minitest::Assertions
+ include SnapDiff::Minitest::Assertions
test "homepage" do
visit '/'
@@ -179,7 +179,7 @@ end
| Concept | BackstopJS | capybara-screenshot-diff |
|---------|-----------|-------------------------|
| Language | JavaScript + Node | Ruby (runs in test suite) |
-| Dependencies | Node, Puppeteer/Chromium | Ruby gems + optional libvips |
+| Dependencies | Node, Puppeteer/Chromium | Ruby gems + libvips (system package) |
| Test runner | Standalone CLI | Minitest, RSpec, Cucumber |
| Selectors | CSS selectors for scenarios | CSS selectors for crop/skip_area |
| Viewports | Per-scenario config | Global `window_size` setting |
@@ -231,8 +231,8 @@ end
## General Migration Checklist
- [ ] Remove old gem/npm dependencies
-- [ ] Add `capybara-screenshot-diff` to Gemfile
-- [ ] Require the appropriate adapter (`minitest`, `rspec`, or `cucumber`)
+- [ ] Add `snap_diff-capybara` to Gemfile
+- [ ] Require the appropriate integration (`snap_diff/integrations/minitest`, `…/rspec`, or `…/cucumber`)
- [ ] Replace screenshot calls with `screenshot` / `match_screenshot`
- [ ] Configure `window_size` for consistent viewport dimensions
- [ ] Set `tolerance` or `perceptual_threshold` if your previous tool had a mismatch threshold
@@ -262,11 +262,9 @@ screenshot 'step2'
### "My tests are slow now"
-Use the VIPS driver for ~50ms comparisons per image:
-```ruby
-gem 'ruby-vips'
-Capybara::Screenshot::Diff.driver = :vips
-```
+Comparison runs on libvips (~50ms per image) and there is nothing to select — `ruby-vips` is a
+runtime dependency of the gem. If comparisons are the slow part, the usual cause is
+`stability_time_limit`: keep it low (0.1–0.5s) or use `disable_animations` instead.
### "The diffs look different from what I'm used to"
@@ -280,7 +278,7 @@ Start with default settings, then adjust `tolerance` or `perceptual_threshold` b
## Need Help?
-- [Architecture Overview](docs/architecture.md) — understanding how comparisons work
-- [Configuration Reference](docs/configuration.md) — all available options
-- [CI Integration](docs/ci-integration.md) — setting up in CI
+- [Architecture Overview](architecture.md) — understanding how comparisons work
+- [Configuration Reference](configuration.md) — all available options
+- [CI Integration](ci-integration.md) — setting up in CI
- [GitHub Issues](https://github.com/snap-diff/snap_diff-capybara/issues) — ask questions
diff --git a/docs/organization.md b/docs/organization.md
index 541f6f27..f347cbaa 100644
--- a/docs/organization.md
+++ b/docs/organization.md
@@ -149,7 +149,7 @@ Often it is useful to test your app using different browsers. To avoid the
screenshots for different Capybara drivers to overwrite each other, set
```ruby
-Capybara::Screenshot.add_driver_path = true
+SnapDiff.config.add_driver_path = true
```
The example above will then save your screenshots like this
@@ -177,7 +177,7 @@ the screen shots differ. To avoid the screenshots for different OSs to
overwrite each other, set
```ruby
-Capybara::Screenshot.add_os_path = true
+SnapDiff.config.add_os_path = true
```
The example above will then save your screenshots like this
diff --git a/docs/reporters.md b/docs/reporters.md
index ab4c1c5b..875fa639 100644
--- a/docs/reporters.md
+++ b/docs/reporters.md
@@ -6,8 +6,7 @@ Generate an interactive Web UI report of screenshot differences:
```ruby
# Add to test_helper.rb — one line, that's it
-require 'snap_diff/reporters/html' # canonical
-# require 'capybara_screenshot_diff/reporters/html' # legacy, same thing
+require 'snap_diff/reporters/html'
```
After running tests, open the report (generated only when there are failures):
@@ -44,8 +43,7 @@ class MyReporter
end
# Register in test_helper.rb
-SnapDiff::Reporting.register(MyReporter.new) # canonical — appends under the mutex
-# CapybaraScreenshotDiff.reporters << MyReporter.new # legacy, same list, skips the lock
+SnapDiff::Reporting.register(MyReporter.new) # appends under the mutex
```
Reporters are notified before assertions are cleared on each test teardown. `finalize` runs from
diff --git a/docs/snapdiff.md b/docs/snapdiff.md
index d431afe6..e48bae1a 100644
--- a/docs/snapdiff.md
+++ b/docs/snapdiff.md
@@ -1,15 +1,11 @@
# SnapDiff — the canonical API
-Everything in this gem lives under `SnapDiff` since v2. This page is the SnapDiff-native
-reference: setup, configuration, the object map, and the extension points — all using canonical
-names only.
+Everything in this gem lives under `SnapDiff`. This page is the reference: setup, configuration,
+the object map, and the extension points.
-The legacy `Capybara::Screenshot::Diff` / `CapybaraScreenshotDiff` names still work — they resolve
-to the same objects — and the rest of the docs still teach them. The first legacy API a process
-touches prints one migration notice; on top of that, *lazily shimmed* constants warn once each.
-Some legacy names are silent by design. [UPGRADING.md](UPGRADING.md#deprecation-warnings) lists
-exactly which is which. Nothing here replaces a working setup — it is what you write for **new** code.
-For migrating an existing suite, see [UPGRADING.md](UPGRADING.md).
+2.1 deleted the v1 `Capybara::Screenshot::Diff` / `CapybaraScreenshotDiff` namespaces outright —
+they are `NameError`s now, not deprecations. Migrating a suite that still uses them? See
+[UPGRADING.md](UPGRADING.md#upgrading-to-v21).
## Quick start
@@ -97,14 +93,13 @@ loads the Minitest integration. See
## Configuration
-All 27 settings live on one flat object, `SnapDiff.config` (a `SnapDiff::Config`).
+All 25 settings live on one flat object, `SnapDiff.config` (a `SnapDiff::Config`).
```ruby
# test_helper.rb / rails_helper.rb
SnapDiff.configure do |config|
config.window_size = [1280, 1024]
config.tolerance = 0.0005
- config.driver = :vips
config.save_path = "doc/screenshots"
end
@@ -113,24 +108,10 @@ SnapDiff.config.hide_caret = true
SnapDiff.config.tolerance # => 0.0005
```
-`SnapDiff::Config` **is** the storage. The legacy `Capybara::Screenshot.*` and
-`Capybara::Screenshot::Diff.*` accessors are thin delegators onto it — one storage, two views —
-so a write through either surface is immediately visible through the other:
-
-```ruby
-SnapDiff.config.window_size = [1280, 1024]
-Capybara::Screenshot.window_size # => [1280, 1024]
-```
-
-`SnapDiff.start` is the same call shape as the old `Capybara::Screenshot::Diff.configure`, if
-you prefer the two-holder form:
-
-```ruby
-SnapDiff.start do |screenshot, diff|
- screenshot.window_size = [1280, 1024]
- diff.tolerance = 0.0005
-end
-```
+`SnapDiff::Config` **is** the storage — one eagerly-created instance, every setting a plain
+`attr_accessor`. There is no second view of it: the v1 `Capybara::Screenshot.*` /
+`Capybara::Screenshot::Diff.*` delegators and the two-holder `SnapDiff.start` block were removed
+in 2.1.
Three derived, read-only values are computed from the settings above:
@@ -141,9 +122,9 @@ Three derived, read-only values are computed from the settings above:
| `SnapDiff.config.default_options` | The capture/compare defaults handed to `SnapDiff::Comparison` |
Every option's meaning is documented in the
-[Configuration Reference](configuration.md) — the names are identical, only the receiver differs.
-The one rename: `Capybara::Screenshot.enabled` is `SnapDiff.config.screenshot_enabled`, because
-`SnapDiff.config.enabled` is taken by `Capybara::Screenshot::Diff.enabled`.
+[Configuration Reference](configuration.md). One name differs from its v1 spelling:
+`screenshot_enabled` is the old `Capybara::Screenshot.enabled`, because the bare `enabled` is
+taken by the old `Capybara::Screenshot::Diff.enabled`.
## Object map
@@ -153,7 +134,7 @@ integration require; a few objects need their own require, noted below.
| Object | What it is for |
|--------|----------------|
| `SnapDiff.config`, `SnapDiff::Config` | Every setting, one flat object. The storage. |
-| `SnapDiff.configure`, `SnapDiff.start` | Config block helpers (consolidated / v1 shape) |
+| `SnapDiff.configure` | Config block helper — the single config entry point |
| `SnapDiff.compare` | Compare two image files directly, no browser |
| `SnapDiff::Comparison` | The layered comparison engine (ex `ImageCompare`) |
| `SnapDiff::Comparison::Images` | Frozen bundle a comparison operates on: both images, their paths, the driver and the options |
@@ -165,8 +146,6 @@ integration require; a few objects need their own require, noted below.
| `SnapDiff::ExpectationNotMet` | A screenshot did not match its baseline |
| `SnapDiff::UnstableImage` | No stable capture within `stability_time_limit` / `wait` |
| `SnapDiff::WindowSizeMismatchError` | Browser window is not the configured `window_size` |
-| `SnapDiff::Driver` | Mixin with the shared driver defaults (`require "snap_diff/driver"`) — **removed in 2.1** |
-| `SnapDiff::Drivers` | Driver factory and registry — `.for`, `.loaded`, `.available` — **removed in 2.1** |
| `SnapDiff::Reporting` | Process-global reporter lifecycle (`require "snap_diff/reporting"`) |
| `SnapDiff::Reporters::HTML` | The interactive HTML report (`require "snap_diff/reporters/html"`) |
| `SnapDiff::Reporters::Default` | Builds the annotated diff images and the failure message |
@@ -262,73 +241,35 @@ SnapDiff.reset # always: notifies reporters, clears th
SnapDiff::Reporting.finalize!
```
-## Custom drivers
-
-> **Removed in 2.1 — no replacement.** libvips becomes the only backend, and the driver
-> abstraction goes with the choice: the `SnapDiff::Driver` mixin, the
-> `SnapDiff::Drivers.loaded` registry, `SnapDiff::Drivers.available`, and selecting a driver
-> by name. In 2.0 all of it still works and warns once per process (silence with
-> `SnapDiff.silence_deprecations = true` or `SNAP_DIFF_SILENCE_DEPRECATIONS=1`). Nothing
-> here migrates to a 2.1 shape — there is no 2.1 shape. If you maintain a driver, say so on
-> [#166](https://github.com/snap-diff/snap_diff-capybara/issues/166) before 2.1 ships.
-
-A driver is a plain object that does the image work. Include `SnapDiff::Driver` for the shared
-defaults, then implement the operations the comparison engine calls:
+## Custom drivers — removed in 2.1
-```ruby
-require "snap_diff/driver"
-
-class MyDriver
- include SnapDiff::Driver
+**There is no migration path, and that is deliberate.** 2.1 removed the driver abstraction
+whole: the `SnapDiff::Driver` mixin, the `SnapDiff::Drivers.loaded` registry,
+`SnapDiff::Drivers.available` detection, the `driver:` setting, and `driver: :auto`. libvips
+is the only backend, `ruby-vips` is a runtime dependency of this gem, and
+`SnapDiff::Drivers::VipsDriver` is wired in directly.
- # Provided by the mixin, override only if your image objects differ:
- # width_for(image), height_for(image), dimension(image),
- # image_area_size(image), same_dimension?(comparison), supports?(feature)
+A third-party driver stops working on 2.1 and nothing replaces it. One backend is what keeps
+the comparison engine honest — every option means one thing, and the reported figures come
+from one implementation. If you maintain a driver, say so on
+[the issue tracker](https://github.com/snap-diff/snap_diff-capybara/issues); that is the only
+thing that can reopen this.
- # Implement (this is what both bundled drivers implement):
- # from_file(path), load_images(base_path, new_path), save_image_to(image, path)
- # same_pixels?(comparison), find_difference_region(comparison)
- # crop(region, image), resize_image_to(image, w, h)
- # add_black_box(image, region), draw_rectangles(images, region, ...)
-end
-```
-
-`supports?(feature)` is just `respond_to?(feature)` — the engine uses it to skip optional
-operations. The VIPS driver additionally implements `filter_image_with_median`, `merge`,
-`highlight_mask` and `difference_level`; ChunkyPNG does not, and `supports?` is how that is
-detected. Look at `lib/snap_diff/drivers/chunky_png_driver.rb` for the smaller of the two
-reference implementations.
-
-### Registration
-
-`SnapDiff::Drivers.loaded` is the registry: a mutable `name => driver class` hash. Register by
-writing into it, then select the driver by that name:
-
-```ruby
-SnapDiff::Drivers.loaded[:my_driver] = MyDriver
-
-SnapDiff.config.driver = :my_driver # globally
-screenshot "index", driver: :my_driver # or per screenshot
-```
-
-Resolution goes through `SnapDiff::Drivers.for`, which looks the symbol up in `loaded` and calls
-`.new` on the class — **your driver class must be instantiable with no arguments**. A pre-built
-instance skips the registry entirely:
-
-```ruby
-screenshot "index", driver: MyDriver.new # any non-Symbol is used as-is
-```
+Everything the abstraction was used for from the outside has a direct answer:
-`SnapDiff::Drivers.available` is the *detected* list (`[:vips, :chunky_png]`, filled at load time
-by probing for the `ruby-vips` and `chunky_png` gems). It is a read-only view of detection, not
-the registry — registering a custom driver does not add it there, and `driver: :auto` picks
-`available.first`. Custom drivers must always be named explicitly.
+| You used | Now |
+|---|---|
+| `SnapDiff.config.driver = :vips` | delete the line |
+| `screenshot "index", driver: :vips` | delete the option |
+| `driver: :auto` | delete it — there is one backend |
+| `SnapDiff::Drivers.available` to branch on what is installed | nothing to branch on; a missing `ruby-vips` is a Bundler resolution error |
+| `SnapDiff::Drivers.loaded[:mine] = MyDriver` | no replacement |
## Related
-- [Framework Setup](framework-setup.md) — the same three integrations under their legacy names
+- [Framework Setup](framework-setup.md) — the three integrations, one page each
- [Configuration Reference](configuration.md) — what every option does
-- [Image Processing Drivers](drivers.md) — VIPS vs ChunkyPNG, perceptual threshold
+- [Image Processing](drivers.md) — libvips, perceptual threshold, tolerance
- [Web UI & Custom Reporters](reporters.md) — the HTML report in detail
- [Architecture](architecture.md) — how the pieces fit together internally
- [UPGRADING.md](UPGRADING.md) — migrating an existing suite off the legacy names
diff --git a/docs/thread_safety.md b/docs/thread_safety.md
index db007c16..f3c0ad79 100644
--- a/docs/thread_safety.md
+++ b/docs/thread_safety.md
@@ -59,7 +59,7 @@ Each thread gets its own `ScreenshotNamer` via the per-thread registry, so count
## Global Configuration
-Configuration uses `mattr_accessor` and should be set once before tests run. Do not mutate config during parallel execution.
+Configuration is one process-wide `SnapDiff::Config` instance of plain `attr_accessor`s, reachable as `SnapDiff.config`. Set it once before tests run; do not mutate it during parallel execution.
## Parallel Test Lifecycle
@@ -73,10 +73,10 @@ Configuration uses `mattr_accessor` and should be set once before tests run. Do
```ruby
parallelize(workers: :number_of_processors, with: :threads)
-Capybara::Screenshot::Diff.configure do |screenshot, diff|
- screenshot.window_size = [1280, 1024]
- screenshot.save_path = "doc/screenshots"
- diff.tolerance = 0.001
+SnapDiff.configure do |config|
+ config.window_size = [1280, 1024]
+ config.save_path = "doc/screenshots"
+ config.tolerance = 0.001
end
```
@@ -100,10 +100,9 @@ Do not:
Runtime state is thread-local (above), but *loading* the gem is a separate
concern. The require graph is deliberately acyclic: `lib/snap_diff/*` units
-depend only on the config-storage leaf (`snap_diff/config`, which the legacy
-view `capybara/screenshot/diff/config_legacy` requires) and specific sibling
-units, the umbrella files depend on the units, and nothing requires back up
-the chain.
+depend only on the config-storage leaf (`snap_diff/config`) and specific
+sibling units, the entry points depend on the units, and nothing requires
+back up the chain.
Eager mutual requires between entry points are forbidden, even guarded ones:
per-thread "loading" flags cannot serialize Ruby's process-global per-file
diff --git a/gems.rb b/gems.rb
index 7637ae78..ff700c13 100644
--- a/gems.rb
+++ b/gems.rb
@@ -7,10 +7,8 @@
gem "rake"
-# Image processing libraries
-gem "chunky_png", ">= 1.3", require: false
-gem "oily_png", platform: :ruby, git: "https://github.com/wvanbergen/oily_png", ref: "44042006e79efd42ce4b52c1d78a4c70f0b4b1b2"
-gem "ruby-vips", require: false
+# ruby-vips is a gemspec runtime dependency since 2.1 (the only backend), so
+# it is not listed here. chunky_png/oily_png went with the chunky_png driver.
group :test do
gem "capybara", ">= 3.26"
diff --git a/lib/capybara-screenshot-diff.rb b/lib/capybara-screenshot-diff.rb
index dff8d102..6855b883 100644
--- a/lib/capybara-screenshot-diff.rb
+++ b/lib/capybara-screenshot-diff.rb
@@ -1,3 +1,13 @@
# frozen_string_literal: true
-require "capybara_screenshot_diff/minitest"
+# Entry point for the `capybara-screenshot-diff` GEM NAME.
+#
+# This is not part of the v1 namespace that 2.1 removed -- it is the file
+# Bundler looks for when a Gemfile says `gem "capybara-screenshot-diff"`.
+# Bundler.require requires the gem's own name, and its dash->slash fallback
+# ("capybara/screenshot/diff") no longer exists, so without this file a Rails
+# user gets a silent no-op and a confusing NameError later.
+#
+# The gem ships under two names with identical content; see
+# lib/snap_diff-capybara.rb for the other one.
+require "snap_diff/integrations/minitest"
diff --git a/lib/capybara/screenshot/diff.rb b/lib/capybara/screenshot/diff.rb
index cf1931d3..91bd7c26 100644
--- a/lib/capybara/screenshot/diff.rb
+++ b/lib/capybara/screenshot/diff.rb
@@ -1,3 +1,6 @@
# frozen_string_literal: true
-require "capybara-screenshot-diff"
+# v1 require path, kept as a one-line alias entry (ADR-008 amendment).
+# The tree that used to live under lib/capybara/screenshot/diff/ is gone;
+# this file is the require line, not the implementation.
+require "capybara_screenshot_diff"
diff --git a/lib/capybara/screenshot/diff/annotation_service.rb b/lib/capybara/screenshot/diff/annotation_service.rb
deleted file mode 100644
index e3c36727..00000000
--- a/lib/capybara/screenshot/diff/annotation_service.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/annotation_service"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/area_calculator.rb b/lib/capybara/screenshot/diff/area_calculator.rb
deleted file mode 100644
index b21ef790..00000000
--- a/lib/capybara/screenshot/diff/area_calculator.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/area_calculator"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/browser_helpers.rb b/lib/capybara/screenshot/diff/browser_helpers.rb
deleted file mode 100644
index d023d60d..00000000
--- a/lib/capybara/screenshot/diff/browser_helpers.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/browser_helpers"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/config_legacy.rb b/lib/capybara/screenshot/diff/config_legacy.rb
deleted file mode 100644
index 0da6e61a..00000000
--- a/lib/capybara/screenshot/diff/config_legacy.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy Capybara::Screenshot / Capybara::Screenshot::Diff config surface.
-#
-# Nothing but requires is left here. The storage is SnapDiff::Config
-# (ADR-008 step 1, the require leaf of the config graph); the derived values
-# (active?, screenshot_area, default_options) live there too since step 7b;
-# and the old accessor names, Diff.configure/.compare, SnapDiff.start and
-# the AVAILABLE_DRIVERS alias are generated by snap_diff/legacy_shims -- the
-# one file that holds the v1 surface as code, so that the canonical core
-# needs nothing from this tree and 3.0 can delete both together. The v1
-# surface (Capybara::Screenshot.window_size = ..., Diff.configure { ... },
-# Diff.compare) keeps working unchanged: one storage, two views.
-#
-# Load order: requiring snap_diff/config first also eagerly evaluates the
-# require-time defaults (ENV["CI"] for fail_if_new, Rails.root/pwd for
-# root) at this same load moment, exactly when the old mattr_accessor
-# default blocks used to run. Neither file requires back here, so the graph
-# stays acyclic.
-require "snap_diff/config"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/cucumber.rb b/lib/capybara/screenshot/diff/cucumber.rb
deleted file mode 100644
index 3ad15ef7..00000000
--- a/lib/capybara/screenshot/diff/cucumber.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy (v1-documented) entry point: like every other legacy entry, it must
-# load the legacy surface too -- requiring only the canonical integration
-# left CapybaraScreenshotDiff half-present (module defined, but .verify /
-# .reset / .reporters / ... gone) for v1 users of this path.
-require "capybara_screenshot_diff/cucumber"
diff --git a/lib/capybara/screenshot/diff/difference.rb b/lib/capybara/screenshot/diff/difference.rb
deleted file mode 100644
index 7d9bf46d..00000000
--- a/lib/capybara/screenshot/diff/difference.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/comparison_result"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/drivers.rb b/lib/capybara/screenshot/diff/drivers.rb
deleted file mode 100644
index 6699ef79..00000000
--- a/lib/capybara/screenshot/diff/drivers.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-# The forwarded module is the same object, so Drivers.for and the
-# Drivers::VipsDriver / Drivers::ChunkyPNGDriver constants keep resolving
-# through the old name.
-require "snap_diff/drivers"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/drivers/base_driver.rb b/lib/capybara/screenshot/diff/drivers/base_driver.rb
deleted file mode 100644
index 22e4f4a9..00000000
--- a/lib/capybara/screenshot/diff/drivers/base_driver.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-# frozen_string_literal: true
-
-# BaseDriver was dissolved into the SnapDiff::Driver mixin (ADR-004 v2
-# step 4); since step 6 the old name resolves lazily via
-# snap_diff/legacy_shims' const_missing, with a deprecation warning.
-# Note it is now a module -- `class MyDriver < BaseDriver` becomes
-# `include SnapDiff::Driver`.
-require "snap_diff/driver"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb b/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb
deleted file mode 100644
index 070acede..00000000
--- a/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the module alias in capybara/screenshot/diff/drivers.rb
-# makes ChunkyPNGDriver reachable as
-# Capybara::Screenshot::Diff::Drivers::ChunkyPNGDriver.
-require "capybara/screenshot/diff/drivers"
-require "snap_diff/drivers/chunky_png_driver"
diff --git a/lib/capybara/screenshot/diff/drivers/vips_driver.rb b/lib/capybara/screenshot/diff/drivers/vips_driver.rb
deleted file mode 100644
index fe8c11aa..00000000
--- a/lib/capybara/screenshot/diff/drivers/vips_driver.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the module alias in capybara/screenshot/diff/drivers.rb
-# makes VipsDriver reachable as Capybara::Screenshot::Diff::Drivers::VipsDriver.
-require "capybara/screenshot/diff/drivers"
-require "snap_diff/drivers/vips_driver"
diff --git a/lib/capybara/screenshot/diff/image_compare.rb b/lib/capybara/screenshot/diff/image_compare.rb
deleted file mode 100644
index 19863e3e..00000000
--- a/lib/capybara/screenshot/diff/image_compare.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder for SnapDiff::Comparison (ex-ImageCompare). Requiring
-# this path must keep providing everything the pre-move image_compare.rb did:
-# snap_diff/comparison pulls in the ComparisonResult and Drivers units, and the
-# shims keep ::Difference, ::Drivers, ::Comparison (the images struct) and
-# LOADED_DRIVERS resolvable.
-require "snap_diff/comparison"
-require "snap_diff/legacy_shims"
-
-# Capybara::Screenshot::Diff::Comparison (the images-holder struct) is a
-# documented user-facing name that adopters feature-detect with
-# defined?/const_defined?, so it is assigned EAGERLY rather than shimmed --
-# const_defined? never triggers const_missing. That assignment now lives in
-# snap_diff/legacy_shims (required above), with the rest of the v1 surface,
-# so `require "snap_diff"` alone provides it too.
diff --git a/lib/capybara/screenshot/diff/image_preprocessor.rb b/lib/capybara/screenshot/diff/image_preprocessor.rb
deleted file mode 100644
index f97017d2..00000000
--- a/lib/capybara/screenshot/diff/image_preprocessor.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/image_preprocessor"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/os.rb b/lib/capybara/screenshot/diff/os.rb
deleted file mode 100644
index 376b6791..00000000
--- a/lib/capybara/screenshot/diff/os.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder. `Capybara::Screenshot::Os` is an EAGER same-object
-# alias assigned in snap_diff/legacy_shims -- the one file every entry point
-# loads, canonical ones included, so a half-migrated app keeps the name.
-require "snap_diff/os"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/region.rb b/lib/capybara/screenshot/diff/region.rb
deleted file mode 100644
index 91e1bb49..00000000
--- a/lib/capybara/screenshot/diff/region.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: snap_diff/region also defines the eager top-level
-# `Region` alias.
-require "snap_diff/region"
diff --git a/lib/capybara/screenshot/diff/reporters/default.rb b/lib/capybara/screenshot/diff/reporters/default.rb
deleted file mode 100644
index af50dbae..00000000
--- a/lib/capybara/screenshot/diff/reporters/default.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder for SnapDiff::Reporters::Default.
-# `Capybara::Screenshot::Diff::Reporters::Default` is an EAGER same-object
-# alias assigned in snap_diff/legacy_shims -- the one file every entry point
-# loads, canonical ones included, so a half-migrated app keeps the name.
-require "snap_diff/reporters/default"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/screenshot_matcher.rb b/lib/capybara/screenshot/diff/screenshot_matcher.rb
deleted file mode 100644
index ad603917..00000000
--- a/lib/capybara/screenshot/diff/screenshot_matcher.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/screenshot_matcher"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/screenshoter.rb b/lib/capybara/screenshot/diff/screenshoter.rb
deleted file mode 100644
index 8c6a05e0..00000000
--- a/lib/capybara/screenshot/diff/screenshoter.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/screenshoter"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/stable_screenshoter.rb b/lib/capybara/screenshot/diff/stable_screenshoter.rb
deleted file mode 100644
index 03f4271a..00000000
--- a/lib/capybara/screenshot/diff/stable_screenshoter.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/stable_screenshoter"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/utils.rb b/lib/capybara/screenshot/diff/utils.rb
deleted file mode 100644
index 858a78eb..00000000
--- a/lib/capybara/screenshot/diff/utils.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/utils"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/vcs.rb b/lib/capybara/screenshot/diff/vcs.rb
deleted file mode 100644
index 5fcea3bf..00000000
--- a/lib/capybara/screenshot/diff/vcs.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/vcs"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara/screenshot/diff/version.rb b/lib/capybara/screenshot/diff/version.rb
deleted file mode 100644
index 0d87948d..00000000
--- a/lib/capybara/screenshot/diff/version.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-# frozen_string_literal: true
-
-# Capybara::Screenshot::Diff::VERSION is a documented name adopters read
-# directly, so it is assigned EAGERLY rather than shimmed -- const_defined?
-# never triggers const_missing. That assignment lives in
-# snap_diff/legacy_shims (required below) with the rest of the v1 surface,
-# because this file is no longer on any entry point's require path: the core
-# reads SnapDiff::VERSION, and so does the gemspec. Assigning it here too
-# would be a duplicate-constant warning, not a second safety net.
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara_screenshot_diff.rb b/lib/capybara_screenshot_diff.rb
index 7ec0d800..ab8dbb11 100644
--- a/lib/capybara_screenshot_diff.rb
+++ b/lib/capybara_screenshot_diff.rb
@@ -1,45 +1,9 @@
# frozen_string_literal: true
-require "capybara/dsl"
-require "capybara/screenshot/diff/config_legacy"
-require "capybara/screenshot/diff/version"
-require "capybara/screenshot/diff/os"
-require "capybara/screenshot/diff/browser_helpers"
-require "capybara/screenshot/diff/utils"
-require "capybara/screenshot/diff/image_compare"
-require "capybara_screenshot_diff/snap_manager"
-require "capybara_screenshot_diff/snap"
-require "capybara/screenshot/diff/screenshoter"
-require "capybara/screenshot/diff/stable_screenshoter"
-require "capybara/screenshot/diff/vcs"
-require "capybara/screenshot/diff/area_calculator"
-require "capybara/screenshot/diff/image_preprocessor"
-require "capybara/screenshot/diff/annotation_service"
-require "capybara_screenshot_diff/screenshot_namer"
-require "capybara_screenshot_diff/screenshot_assertion"
-require "capybara_screenshot_diff/attempts_reporter"
-require "capybara/screenshot/diff/screenshot_matcher"
-require "capybara/screenshot/diff/reporters/default"
-
-require "capybara_screenshot_diff/error_with_filtered_backtrace"
-require "snap_diff/errors"
-
-# RED_RGBA / ORANGE_RGBA moved to SnapDiff (snap_diff/annotation_service) so
-# the bare "snap_diff" entry gets them too; the old names resolve via
-# snap_diff/legacy_shims with a deprecation warning.
+# v1 require path, kept as a one-line alias entry (ADR-008 amendment): four
+# of six discoverable real users import this gem under its v1 names, and a
+# LoadError here fires before any constant alias could help them.
#
-# The four error classes (CapybaraScreenshotDiffError, ExpectationNotMet,
-# UnstableImage, WindowSizeMismatchError) used to be assigned here as EAGER
-# same-object aliases. They still are eager -- just from
-# snap_diff/legacy_shims, so a canonical-only require gets them too.
-require "snap_diff/legacy_shims"
-
-require "capybara_screenshot_diff/dsl"
-
-# Eager, not autoload: several lib/snap_diff/* units above (Os,
-# Screenshoter, ...) reopen `module SnapDiff` while loading, which cancels
-# any registered `autoload :SnapDiff` before it ever fires (Ruby resolves
-# the constant the first time anything reopens it, autoload or not) --
-# so SnapDiff.start/.compare/.config would silently never be defined
-# without this. Safe eagerly: snap_diff.rb never requires this file back.
-require "snap_diff"
+# It loads the minitest integration for the same reason the gem-name entries
+# do -- that is what the v1 entry point always activated.
+require "snap_diff/integrations/minitest"
diff --git a/lib/capybara_screenshot_diff/attempts_reporter.rb b/lib/capybara_screenshot_diff/attempts_reporter.rb
deleted file mode 100644
index 159b63aa..00000000
--- a/lib/capybara_screenshot_diff/attempts_reporter.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/attempts_reporter"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara_screenshot_diff/cucumber.rb b/lib/capybara_screenshot_diff/cucumber.rb
index 523e5bad..dbbeefbd 100644
--- a/lib/capybara_screenshot_diff/cucumber.rb
+++ b/lib/capybara_screenshot_diff/cucumber.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
-# Legacy entry point: pre-move, this path transitively loaded the whole gem
-# (via dsl -> umbrella). Preserve that contract for consumers who require
-# only this file.
-require "capybara_screenshot_diff"
-
+# v1 require path, kept as a one-line alias entry (ADR-008 amendment).
+# No discoverable user, but its two siblings are kept: an entry point that
+# LoadErrors while the other two work is a worse surface than either choice.
require "snap_diff/integrations/cucumber"
diff --git a/lib/capybara_screenshot_diff/dsl.rb b/lib/capybara_screenshot_diff/dsl.rb
index 33535a3e..6c7d4a46 100644
--- a/lib/capybara_screenshot_diff/dsl.rb
+++ b/lib/capybara_screenshot_diff/dsl.rb
@@ -1,12 +1,5 @@
# frozen_string_literal: true
+# v1 require path, kept as a one-line alias entry (ADR-008 amendment).
+# CapybaraScreenshotDiff::DSL is the same object as SnapDiff::DSL.
require "snap_diff/dsl"
-
-# Deliberately EAGER and silent (v2 step 6 exception): DSL is an advertised
-# entry-point constant probed with Object.const_defined? by
-# support_load_probe_test.rb, and const_defined? never triggers
-# const_missing -- a lazy shim would break that contract. See
-# snap_diff/legacy_shims.rb for the full exception list.
-module CapybaraScreenshotDiff
- DSL = SnapDiff::DSL
-end
diff --git a/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb b/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb
deleted file mode 100644
index c4c32fea..00000000
--- a/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/error_with_filtered_backtrace"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara_screenshot_diff/minitest.rb b/lib/capybara_screenshot_diff/minitest.rb
index 5c993cf4..db64bfc3 100644
--- a/lib/capybara_screenshot_diff/minitest.rb
+++ b/lib/capybara_screenshot_diff/minitest.rb
@@ -1,14 +1,5 @@
# frozen_string_literal: true
-# Legacy entry point: pre-move, this path transitively loaded the whole gem
-# (via dsl -> umbrella). Preserve that contract for consumers who require
-# only this file.
-require "capybara_screenshot_diff"
-
+# v1 require path, kept as a one-line alias entry (ADR-008 amendment).
+# bootstrap_form (1,643 stars) has exactly this line in its system test case.
require "snap_diff/integrations/minitest"
-
-module CapybaraScreenshotDiff
- module Minitest
- Assertions = SnapDiff::Minitest::Assertions
- end
-end
diff --git a/lib/capybara_screenshot_diff/reporters/html.rb b/lib/capybara_screenshot_diff/reporters/html.rb
deleted file mode 100644
index 84f06c87..00000000
--- a/lib/capybara_screenshot_diff/reporters/html.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/reporters/html"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara_screenshot_diff/rspec.rb b/lib/capybara_screenshot_diff/rspec.rb
index d684fb96..d01ed1b8 100644
--- a/lib/capybara_screenshot_diff/rspec.rb
+++ b/lib/capybara_screenshot_diff/rspec.rb
@@ -1,8 +1,5 @@
# frozen_string_literal: true
-# Legacy entry point: pre-move, this path transitively loaded the whole gem
-# (via dsl -> umbrella). Preserve that contract for consumers who require
-# only this file.
-require "capybara_screenshot_diff"
-
+# v1 require path, kept as a one-line alias entry (ADR-008 amendment).
+# nerdgeschoss/app has exactly this line in its spec helper.
require "snap_diff/integrations/rspec"
diff --git a/lib/capybara_screenshot_diff/screenshot_assertion.rb b/lib/capybara_screenshot_diff/screenshot_assertion.rb
deleted file mode 100644
index f11473b9..00000000
--- a/lib/capybara_screenshot_diff/screenshot_assertion.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-# frozen_string_literal: true
-
-require "snap_diff/screenshot_assertion"
-require "snap_diff/reporting"
-# ScreenshotAssertion / AssertionRegistry forward lazily (with deprecation
-# warnings) via snap_diff/legacy_shims' const_missing since v2 step 6.
-require "snap_diff/legacy_shims"
-
-# Since ADR-008 step 6 every method here is a thin forwarder; the canonical
-# homes are SnapDiff (session lifecycle: per-test, `SnapDiff.session` and
-# friends) and SnapDiff::Reporting (reporter lifecycle: process-global,
-# suite-long). Names, arities and object identities are unchanged -- this
-# module stays as the compatibility surface for existing consumers.
-module CapybaraScreenshotDiff
- class << self
- require "forwardable"
- extend Forwardable
-
- # --- Session lifecycle (per-test) -> SnapDiff ---
-
- def registry
- SnapDiff.session
- end
-
- def_delegators :registry, :add_assertion, :assertions, :assertions_present?,
- :failed_assertions, :record_new_screenshot, :new_screenshots,
- :new_screenshots_present?, :screenshot_namer, :verify
-
- # Written out rather than def_delegators so the arities stay 0 (a
- # Forwardable-generated method takes *args, **kwargs, &block).
- def reset
- SnapDiff.reset
- end
-
- def pending_screenshots_message
- SnapDiff.pending_screenshots_message
- end
-
- # --- Reporter lifecycle (process-global, suite-long) -> SnapDiff::Reporting ---
-
- def reporters
- SnapDiff::Reporting.reporters
- end
-
- def reporters_mutex
- SnapDiff::Reporting.mutex
- end
-
- def finalize_reporters!
- SnapDiff::Reporting.finalize!
- end
-
- private
-
- def notify_reporters(assertions)
- SnapDiff::Reporting.notify(assertions)
- end
- end
-end
diff --git a/lib/capybara_screenshot_diff/screenshot_namer.rb b/lib/capybara_screenshot_diff/screenshot_namer.rb
deleted file mode 100644
index 6f6e53b8..00000000
--- a/lib/capybara_screenshot_diff/screenshot_namer.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/screenshot_namer"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara_screenshot_diff/snap.rb b/lib/capybara_screenshot_diff/snap.rb
deleted file mode 100644
index 2ba5e5a8..00000000
--- a/lib/capybara_screenshot_diff/snap.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/snap"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara_screenshot_diff/snap_manager.rb b/lib/capybara_screenshot_diff/snap_manager.rb
deleted file mode 100644
index 6446e9a0..00000000
--- a/lib/capybara_screenshot_diff/snap_manager.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy-name forwarder: the old constant resolves via snap_diff/legacy_shims.
-require "snap_diff/snap_manager"
-require "snap_diff/legacy_shims"
diff --git a/lib/capybara_screenshot_diff/static.rb b/lib/capybara_screenshot_diff/static.rb
deleted file mode 100644
index 5d814184..00000000
--- a/lib/capybara_screenshot_diff/static.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-# frozen_string_literal: true
-
-# Legacy entry point: loads the legacy surface (the umbrella) like its
-# minitest/rspec/cucumber siblings, so consumers who require only this file
-# still get CapybaraScreenshotDiff's session and reporter methods.
-require "capybara_screenshot_diff"
-require "snap_diff/static"
-
-module CapybaraScreenshotDiff
- def self.serve(...)
- SnapDiff.serve(...)
- end
-end
diff --git a/lib/snap_diff-capybara.rb b/lib/snap_diff-capybara.rb
index 2a35777b..c3a5a4db 100644
--- a/lib/snap_diff-capybara.rb
+++ b/lib/snap_diff-capybara.rb
@@ -3,6 +3,5 @@
# Bundler.require entry point for `gem "snap_diff-capybara"`: Bundler
# requires the gem's own name, and its dash->slash fallback ("snap_diff/
# capybara") misses too, so without this file a Rails user gets a silent
-# no-op and a confusing NameError later. Loads what the sibling
-# capybara-screenshot-diff.rb loads.
-require "capybara_screenshot_diff/minitest"
+# no-op and a confusing NameError later.
+require "snap_diff/integrations/minitest"
diff --git a/lib/snap_diff.rb b/lib/snap_diff.rb
index 1bc84e7b..dbfa1492 100644
--- a/lib/snap_diff.rb
+++ b/lib/snap_diff.rb
@@ -27,40 +27,30 @@ def self.assert_single_gem!(loaded_specs = Gem.loaded_specs)
end
SnapDiff.assert_single_gem!
-# This lean entry must never load the umbrella "capybara_screenshot_diff"
-# -- snap_diff_test.rb's "bare require never loads the umbrella" guard
-# enforces it -- so nothing required below may reach back here. None of
-# these requires reaches into lib/capybara* at all, so the canonical entry
-# point is exactly what 3.0 keeps.
-#
# "capybara/dsl" is needed directly (not just transitively) so
# `Capybara.default_max_wait_time` in Config#default_options resolves even
# when "snap_diff" is required standalone (SnapDiffTest's
# "standalone-loadable in a fresh process" regression test).
-#
-# snap_diff/legacy_shims is deliberate and is the ONE line here that 3.0
-# drops: it carries the whole v1 surface (const_missing forwarders, the old
-# mattr_accessors, SnapDiff.start), so a process that only ever requires
-# "snap_diff" still resolves the old Capybara::Screenshot::Diff names --
-# with deprecation warnings -- exactly as it did when this file reached
-# through the capybara/screenshot/diff/* forwarders to get them.
require "capybara/dsl"
require "snap_diff/config"
require "snap_diff/comparison"
-require "snap_diff/legacy_shims"
require "snap_diff/version"
# SnapDiff.session/.reset/.pending_screenshots_message are part of the
# documented core surface (docs/snapdiff.md object map lists them with no
# extra require), so the entry point owns them rather than leaving them to
# whichever integration happens to be loaded.
require "snap_diff/screenshot_assertion"
+# The permanent compatibility surface: the v1 name aliases and the raising
+# stubs for the settings 2.1 removed. Required HERE, from the one file every
+# entry point routes through, because an alias that only some entry points
+# define is worse than none -- see snap_diff/compat.rb for the evidence that
+# put it there.
+require "snap_diff/compat"
-# The canonical namespace for the gem. The old
-# +Capybara::Screenshot::Diff+ constants are same-object const_missing shims
-# (snap_diff/legacy_shims) that warn once per constant per process.
+# The namespace for the gem. 2.1 deleted the v1 implementation trees; what
+# survives of those names is alias-only (snap_diff/compat.rb).
module SnapDiff
- # Compare two images on disk with the configured defaults. Canonical home;
- # +Capybara::Screenshot::Diff.compare+ forwards here.
+ # Compare two images on disk with the configured defaults.
#
# Note the argument order swap: callers pass baseline first (reading
# "compare baseline against current"), Comparison takes current first.
@@ -68,21 +58,26 @@ def self.compare(baseline_path, current_path, **options)
Comparison.new(current_path, baseline_path, config.default_options.merge(options))
end
- # SnapDiff.start -- the v1-shaped two-holder config block -- is defined in
- # snap_diff/legacy_shims (required above), because the holders it yields
- # are the v1 surface and it cannot outlive them.
-
- # Forward-looking configuration: yields the single consolidated
- # {SnapDiff::Config} object instead of the two old holders. Same
- # underlying storage as +start+ / the old mattr_accessors -- this is a
- # different *shape* of the same settings, not a second source of truth.
+ # THE config entry point (ADR-008): yields the single consolidated
+ # {SnapDiff::Config} object. +SnapDiff.start+ yielded the two v1 config
+ # holders, so it could not outlive them -- 2.1 removed it rather than
+ # renaming it.
#
# SnapDiff.configure do |config|
# config.window_size = [1280, 1024]
# config.tolerance = 0.0005
# end
+ #
+ # Yielded TWICE, and that second argument is not decoration. Since
+ # snap_diff/compat.rb aliases +Capybara::Screenshot::Diff+ to this module,
+ # the v1 two-holder form -- configure { |screenshot, diff| ... } --
+ # arrives here. With a one-argument yield it would bind +diff+ to nil and
+ # blow up a few lines later on nil; the two holders collapsed into this one
+ # object, so handing it over twice makes the old shape keep working. A
+ # one-parameter block ignores the extra argument, so the canonical form is
+ # unaffected.
def self.configure
- yield config
+ yield config, config
end
# SnapDiff.config itself is defined in snap_diff/config.rb (the storage
diff --git a/lib/snap_diff/annotation_service.rb b/lib/snap_diff/annotation_service.rb
index 592115fa..4f184c1d 100644
--- a/lib/snap_diff/annotation_service.rb
+++ b/lib/snap_diff/annotation_service.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
module SnapDiff
- # Annotation colors, defined here (not in the capybara_screenshot_diff
- # umbrella) so they resolve in processes that only `require "snap_diff"`.
+ # Annotation colors, defined here (not in an umbrella file) so they resolve
+ # in processes that only `require "snap_diff"`.
RED_RGBA = [255, 0, 0, 255].freeze
ORANGE_RGBA = [255, 192, 0, 255].freeze
diff --git a/lib/snap_diff/comparison.rb b/lib/snap_diff/comparison.rb
index 1a0965a4..d40c0e47 100644
--- a/lib/snap_diff/comparison.rb
+++ b/lib/snap_diff/comparison.rb
@@ -4,9 +4,11 @@
require "fileutils"
require "snap_diff/comparison_result"
-require "snap_diff/drivers"
+# SnapDiff.reject_removed_options! lives there; required directly so this
+# unit keeps working when it is loaded ahead of the entry point.
+require "snap_diff/compat"
+require "snap_diff/drivers/vips_driver"
require "snap_diff/image_preprocessor"
-require "snap_diff/removal"
require "snap_diff/reporters/default"
module SnapDiff
@@ -41,7 +43,7 @@ def skip_area
end
end
- TOLERABLE_OPTIONS = [:tolerance, :color_distance_limit, :shift_distance_limit, :area_size_limit].freeze
+ TOLERABLE_OPTIONS = [:tolerance, :color_distance_limit, :area_size_limit].freeze
attr_reader :driver, :driver_options
attr_reader :image_path, :base_image_path
@@ -53,15 +55,16 @@ def initialize(image_path, base_image_path, options = {})
ensure_files_exist!
+ # THE funnel every per-screenshot options hash passes through, which is
+ # why the removed-option guard lives here rather than in each DSL entry:
+ # an unvalidated hash turned `screenshot "home", shift_distance_limit: 5`
+ # into a silent no-op.
+ SnapDiff.reject_removed_options!(options)
@driver_options = options.freeze
- # The per-comparison half of the shift_distance_limit removal (the
- # global half is Config#shift_distance_limit=). Presence is not enough:
- # config.default_options carries the key on EVERY comparison, nil for
- # everyone who never set it.
- if options[:shift_distance_limit]
- Removal.warn_once(:shift_distance_limit, Removal::SHIFT_DISTANCE_LIMIT_REMOVED)
- end
- @driver = Drivers.for(@driver_options)
+ # One backend since 2.1, constructed rather than looked up: the driver
+ # is stateless, so nothing is gained by threading one instance through
+ # the options hash the way the registry used to.
+ @driver = Drivers::VipsDriver.new
@without_tolerable_options = (driver_options.keys & TOLERABLE_OPTIONS).empty?
end
diff --git a/lib/snap_diff/compat.rb b/lib/snap_diff/compat.rb
new file mode 100644
index 00000000..ea689a79
--- /dev/null
+++ b/lib/snap_diff/compat.rb
@@ -0,0 +1,166 @@
+# frozen_string_literal: true
+
+require "snap_diff/config"
+
+# THE PERMANENT COMPATIBILITY SURFACE (ADR-008 amendment, 2026-08-24).
+#
+# 2.1 stays vips-only -- the driver abstraction, chunky_png and the v1
+# implementation trees are gone and are not coming back. What lives here is
+# only the FAILURE MODE, and it exists because "no demand" turned out to be
+# false when it was finally measured against the live GitHub API:
+#
+# - `CapybaraScreenshotDiff::DSL` / `::Minitest::Assertions` is the entry
+# point 4 of 6 discoverable real users had already migrated TO. Zero were
+# on `SnapDiff::*`. From the outside those names never read as "legacy";
+# until 2.0 they WERE the current API.
+# - 3 of 6 real configs set `driver` explicitly, two of them to `:vips` --
+# asking for the only backend that survives.
+# - `shift_distance_limit`'s one known user guards the writer with
+# `respond_to?`. Deleting the writer is SILENT for them: no error, no
+# warning, their anti-aliasing tolerance quietly gone and a suite that
+# starts failing for reasons no upgrade note can be traced to.
+#
+# So: the names users import survive as aliases, and every seam that really
+# is gone raises with a message naming the replacement. Nothing here restores
+# behaviour -- `driver` selects nothing and `shift_distance_limit` does
+# nothing, they just stop being able to lie about it.
+#
+# EAGER same-object aliases, never `const_missing` shims. `const_defined?`
+# and `defined?` do not trigger `const_missing`, so a lazy shim silently
+# breaks every adopter that feature-detects before including -- a distinction
+# that has already produced one false claim in this project's CHANGELOG.
+#
+# This is the one file under lib/ that core_tree_has_no_legacy_deps_test
+# exempts. It is the compatibility surface; naming the old names is its job.
+module SnapDiff
+ # Named constants rather than inline strings: these messages are what the
+ # affected users see, and compat_surface_test asserts on their content.
+ CHUNKY_PNG_REMOVED =
+ "`driver = :chunky_png` is removed in 2.1: libvips is the only backend. Install system " \
+ "libvips (`apt-get install libvips-dev`, `brew install vips`) and the `ruby-vips` gem, then " \
+ "drop the `driver` setting -- it is accepted and ignored for every other value. " \
+ "See docs/UPGRADING.md."
+
+ SHIFT_DISTANCE_LIMIT_REMOVED =
+ "`shift_distance_limit` is removed in 2.1: only the chunky_png driver ever implemented it, " \
+ "and that driver is gone. libvips has no shift-distance comparison -- drop the setting and " \
+ "tune `tolerance` / `color_distance_limit` instead. See docs/UPGRADING.md."
+
+ class Config
+ # Accept-and-ignore. The two real configs that set this set it to `:vips`,
+ # i.e. they are already asking for what they will get, so silence is the
+ # honest answer for them; `:chunky_png` is the one value the gem can no
+ # longer honour, so it is the one value that raises.
+ def driver=(value)
+ raise ArgumentError, CHUNKY_PNG_REMOVED if value.to_s == "chunky_png"
+ end
+
+ # Always :vips, whatever was assigned -- storing the assignment would let
+ # a config keep claiming a backend choice that does not exist.
+ def driver
+ :vips
+ end
+
+ # A raising stub, not a deletion. The writer is what the one known user
+ # `respond_to?`-guards, so it has to still BE there in order to say no.
+ # nil is let through: it is what "never set" looks like when a config is
+ # copied around, and raising on it would break people who set nothing.
+ def shift_distance_limit=(value)
+ raise ArgumentError, SHIFT_DISTANCE_LIMIT_REMOVED unless value.nil?
+ end
+ end
+
+ class << self
+ # The v1 namespaces are aliases of THIS module (below), so defining the
+ # removed setters here is what puts them back on
+ # `Capybara::Screenshot::Diff.driver=` -- the spelling every real config
+ # actually uses.
+ def driver
+ config.driver
+ end
+
+ def driver=(value)
+ config.driver = value
+ end
+
+ def shift_distance_limit=(value)
+ config.shift_distance_limit = value
+ end
+
+ # @api private
+ #
+ # The other half of the surface a user can write: the per-screenshot
+ # options hash. It was frozen and never validated, so
+ # `screenshot "home", shift_distance_limit: 5` was a silent no-op for
+ # exactly the same reason the writer was. One guard at the funnel every
+ # options hash passes through (Comparison#initialize), not one per call
+ # site -- and it reuses the setters above, so there is one message and one
+ # rule per removed name.
+ def reject_removed_options!(options)
+ self.driver = options[:driver] if options.key?(:driver)
+ self.shift_distance_limit = options[:shift_distance_limit] if options.key?(:shift_distance_limit)
+ end
+ end
+
+ # The v1 error name, kept for `rescue` clauses. Worth its own line: a
+ # NameError inside a rescue fires only when an exception is already in
+ # flight, so it converts someone's real failure into a confusing one at the
+ # moment they can least afford it. Every other v1 error name
+ # (ExpectationNotMet, UnstableImage, WindowSizeMismatchError) is spelled the
+ # same under SnapDiff and comes along with the module alias.
+ CapybaraScreenshotDiffError = Error
+
+ # @api private
+ #
+ # Regenerates the v1 `mattr_accessor` surface as thin delegators onto the one
+ # storage in {SnapDiff.config}.
+ #
+ # GENERATED FROM Config::SETTINGS, deliberately: a hand-written table of 27
+ # rows is a table that drifts the first time someone adds a setting, and the
+ # failure mode of that drift is a NoMethodError on line 1 of a user's test
+ # helper. compat_surface_test walks SETTINGS and fails if any name is
+ # unreachable.
+ #
+ # Both holders get the full set rather than the historical split. The split
+ # would need exactly the hand-maintained table this avoids, and widening a
+ # holder cannot break anyone: it only accepts a spelling that used to raise.
+ #
+ # No deprecation warning: 2.1 deleted the channel that emitted them
+ # (snap_diff/removal.rb, snap_diff/deprecation.rb) and one warning is not
+ # worth resurrecting a subsystem for. These are plain delegators.
+ module Compat
+ # `enabled` is the one name that is not identity-mapped. The two v1 holders
+ # each carried an independent `enabled` (see Config#active?, which reads
+ # both); a flat Config cannot expose two attributes of that name, so the
+ # capture-side one became `screenshot_enabled`. Collapsing them here would
+ # change `active?`, so each holder says which attribute its `enabled` means.
+ def self.install(namespace, enabled_maps_to:)
+ mapping = Config::SETTINGS.to_h { |attr| [attr, attr] }
+ mapping[:enabled] = enabled_maps_to
+
+ mapping.each do |name, attr|
+ # mattr_accessor defined BOTH singleton and instance accessors -- the
+ # instance ones are what `include Capybara::Screenshot::Diff` picks up,
+ # and an include that silently adds nothing is the same class of bug as
+ # a setter that silently does nothing.
+ [namespace, namespace.singleton_class].each do |target|
+ target.define_method(name) { SnapDiff.config.public_send(attr) }
+ target.define_method(:"#{name}=") { |value| SnapDiff.config.public_send(:"#{attr}=", value) }
+ end
+ end
+ end
+ end
+end
+
+# `CapybaraScreenshotDiff::DSL` and `::Minitest::Assertions` come along for
+# free and stay same-object, because the module IS SnapDiff.
+CapybaraScreenshotDiff = SnapDiff
+
+module Capybara
+ module Screenshot
+ Diff = SnapDiff
+ end
+end
+
+SnapDiff::Compat.install(Capybara::Screenshot, enabled_maps_to: :screenshot_enabled)
+SnapDiff::Compat.install(Capybara::Screenshot::Diff, enabled_maps_to: :enabled)
diff --git a/lib/snap_diff/config.rb b/lib/snap_diff/config.rb
index 367e0d3c..886a4770 100644
--- a/lib/snap_diff/config.rb
+++ b/lib/snap_diff/config.rb
@@ -2,31 +2,20 @@
require "pathname"
-# This file is the LEAF of the config require graph (ADR-008 step 1):
-# config_legacy.rb requires it, so it must never require config_legacy nor
-# anything that leads back to either entry point. It also names nothing from
-# the v1 trees at all (3.0 readiness): which legacy accessor each setting is
-# exposed as is snap_diff/legacy_shims' business, and that file is deleted
-# together with lib/capybara* -- see LegacyShims::CONFIG_MAPPING.
+# This file is the LEAF of the config require graph (ADR-008 step 1): it must
+# never require anything that leads back to an entry point.
# Referenced by Config#initialize (screenshoter/manager defaults), which
# runs at the eager Config.new at the bottom of this file, so they must be
# real, already-loaded classes first. Neither requires back here.
-require "snap_diff/removal"
require "snap_diff/screenshoter"
require "snap_diff/snap_manager"
module SnapDiff
- # Flat consolidation of every legacy +Capybara::Screenshot+ /
- # +Capybara::Screenshot::Diff+ setting behind one object:
- # SnapDiff.config.attr .
- #
- # Storage ownership (ADR-008 step 1, inverted from the original v2
- # consolidation): Config IS the single storage. The legacy accessors on
- # +Capybara::Screenshot+ / +Capybara::Screenshot::Diff+ are thin
- # delegators generated by snap_diff/legacy_shims from its CONFIG_MAPPING
- # -- one storage, two views, so a write through either surface is visible
- # through the other structurally, not by synchronization.
+ # Every setting the gem has, behind one object:
+ # SnapDiff.config.attr . Since 2.1 deleted the v1
+ # namespaces this is not just the single storage but the single surface --
+ # {SnapDiff.configure} is the one config entry point (ADR-008).
#
# Default timing contract (pinned by config_default_timing_test.rb):
# every default below is evaluated ONCE, in #initialize, which runs at
@@ -42,13 +31,12 @@ class Config
# used to declare them.
#
# +screenshot_enabled+ is the one name that differs from its legacy
- # spelling: +Capybara::Screenshot.enabled+ and
- # +Capybara::Screenshot::Diff.enabled+ are independent settings (see
- # {#active?}, which reads both) that happened to share a bare name in
- # their own modules. A flat Config can't expose two attributes both
- # called +enabled+, so the Screenshot-side one is renamed here; Diff's
- # keeps the bare +enabled+ name since it's the one most existing
- # configuration touches directly.
+ # spelling. The two v1 holders each carried an independent +enabled+
+ # setting (see {#active?}, which still reads both) under their own
+ # module, and a flat Config cannot expose two attributes both called
+ # +enabled+ -- so the capture-side one became +screenshot_enabled+ and
+ # the comparison-side one kept the bare name, being the one most
+ # existing configuration touches directly.
SETTINGS = %i[
add_driver_path
add_os_path
@@ -70,20 +58,15 @@ class Config
fail_on_difference
color_distance_limit
enabled
- shift_distance_limit
skip_area
- driver
tolerance
perceptual_threshold
screenshoter
manager
].freeze
- # shift_distance_limit is excluded from the generated writers and hand
- # written below (it announces its 2.1 removal); generating it here too
- # would print Ruby's "method redefined" warning on every load.
- attr_accessor(*(SETTINGS - %i[root shift_distance_limit]))
- attr_reader :root, :shift_distance_limit
+ attr_accessor(*(SETTINGS - %i[root]))
+ attr_reader :root
def initialize
# Every setting gets its ivar up front (nil-defaulted ones included)
@@ -92,7 +75,7 @@ def initialize
# only appears on first write would escape that snapshot and leak
# between tests.
SETTINGS.each { |key| instance_variable_set(:"@#{key}", nil) }
- # Capybara::Screenshot side.
+ # Capture side.
@blur_active_element = true
@hide_caret = true
# Raw Rails.root (no coercion), matching the old mattr_reader default;
@@ -101,13 +84,12 @@ def initialize
@save_path = "doc/screenshots"
@screenshot_format = "png"
@capybara_screenshot_options = {}
- # Capybara::Screenshot::Diff side.
+ # Comparison side.
@delayed = true
@fail_if_new = !ENV["CI"].nil? && !ENV["CI"].empty?
@pending_if_new = false
@fail_on_difference = true
@enabled = true
- @driver = :auto
@screenshoter = SnapDiff::Screenshoter
@manager = SnapDiff::SnapManager
end
@@ -116,16 +98,6 @@ def root=(path)
@root = Pathname(path).expand_path
end
- # Overrides the generated accessor above to announce the 2.1 removal
- # (chunky_png-only, and chunky_png goes too). The writer, not the reader:
- # the reader runs on every comparison through #default_options, including
- # for the overwhelming majority who never set this. #initialize seeds the
- # ivar directly, so booting the gem stays silent.
- def shift_distance_limit=(value)
- Removal.warn_once(:shift_distance_limit, Removal::SHIFT_DISTANCE_LIMIT_REMOVED) unless value.nil?
- @shift_distance_limit = value
- end
-
# --- Derived config (ADR-008 step 7b) -------------------------------
# Read-only values computed from the storage above. They used to live
# on the legacy modules; those now one-line forward here.
@@ -152,21 +124,20 @@ def screenshot_area_abs
root / screenshot_area
end
- # ex +Capybara::Screenshot::Diff.default_options+: the capture/compare
- # defaults handed to {SnapDiff::Comparison}. Carries the one literal
- # that is not a stored setting -- the vips tolerance floor.
+ # The capture/compare defaults handed to {SnapDiff::Comparison}. Carries
+ # the one literal that is not a stored setting -- the vips tolerance
+ # floor, now unconditional: 2.1 made libvips the only backend, so the
+ # driver == :vips guard this used to carry was always true.
def default_options
{
area_size_limit: area_size_limit,
color_distance_limit: color_distance_limit,
- driver: driver,
screenshot_format: screenshot_format,
capybara_screenshot_options: capybara_screenshot_options,
perceptual_threshold: perceptual_threshold,
- shift_distance_limit: shift_distance_limit,
skip_area: skip_area,
stability_time_limit: stability_time_limit,
- tolerance: tolerance || ((driver == :vips) ? 0.001 : nil),
+ tolerance: tolerance || 0.001,
# Deliberately LIVE (pinned by config_default_timing_test.rb):
# read at call time, never frozen into storage.
wait: Capybara.default_max_wait_time
diff --git a/lib/snap_diff/deprecation.rb b/lib/snap_diff/deprecation.rb
deleted file mode 100644
index d4941600..00000000
--- a/lib/snap_diff/deprecation.rb
+++ /dev/null
@@ -1,132 +0,0 @@
-# frozen_string_literal: true
-
-# SnapDiff.silence_deprecations? -- the one switch that silences BOTH halves
-# of the story -- lives in snap_diff/removal.rb, not here: the other half
-# (the driver features 2.1 removes) is announced from core files that outlive
-# this one, and they cannot depend on a file the same deletion removes.
-require "snap_diff/removal"
-
-module SnapDiff
- # @api private
- #
- # Internal until the v2 namespace transition; not a public contract.
- #
- # Warn-once-per-subject deprecation engine for the legacy-namespace
- # shims: snap_diff/legacy_shims routes every +const_missing+ hit on an
- # old +Capybara::Screenshot::Diff+ / +CapybaraScreenshotDiff+ constant
- # through {.warn}, so each deprecated name warns exactly once per
- # process (ADR-004's v2 namespace transition).
- module Deprecation
- # Everything under lib/ is "the gem"; the first caller frame outside
- # it is the user code that referenced the deprecated name (same
- # filtering idea as BacktraceFilter in error_with_filtered_backtrace).
- GEM_LIB_DIR = File.expand_path("..", __dir__) + File::SEPARATOR
- # Emission channel: Kernel#warn, not a direct +$stderr.puts+.
- #
- # Kernel#warn delegates to +Warning.warn+ (Ruby >= 2.4), so anything
- # that hooks +Warning.warn+ -- a test suite that raises on warnings, a
- # custom log formatter, Ruby's own -W flag -- sees these messages the
- # same way it sees every other Ruby warning. Writing straight to
- # +$stderr+ would bypass that hook entirely and be invisible to any
- # caller who has customized +Warning+ behavior.
- MUTEX = Mutex.new
- @seen = {}
- @notified = false
- @notice_suppressed = false
-
- # The ONE line a v1 user gets, whichever door they came through. Most of
- # the v1 surface cannot warn per use -- the config accessors are plain
- # delegators and the eager aliases never reach const_missing -- so
- # without this a 2.x app is completely silent right up to the bare
- # NameError it gets on 2.1. Deliberately generic and once per process:
- # an actionable signal, not per-call stderr noise.
- MIGRATION_NOTICE =
- "[snap_diff deprecation] This process uses the v1 `Capybara::Screenshot*` / " \
- "`CapybaraScreenshotDiff*` API. It still works in 2.0 and is REMOVED in 2.1 -- " \
- "see docs/UPGRADING.md for the SnapDiff replacements. Silence with " \
- "`SnapDiff.silence_deprecations = true` or SNAP_DIFF_SILENCE_DEPRECATIONS=1. " \
- "(shown once per process)"
-
- class << self
- # Emit {MIGRATION_NOTICE}, exactly once per process. Called from every
- # v1 entry that can be hooked: the const_missing shims (via {.warn}),
- # the generated legacy config accessors, and `include
- # Capybara::Screenshot[::Diff]`.
- # @return [void]
- def notice
- return if @notified || @notice_suppressed || SnapDiff.silence_deprecations?
-
- first_time = MUTEX.synchronize { @notified ? false : (@notified = true) }
- Kernel.warn(MIGRATION_NOTICE) if first_time
- end
-
- # @api private
- #
- # Suppresses {MIGRATION_NOTICE} for the rest of the process, without
- # touching the per-constant warnings. For hosts that ARE the v1
- # surface rather than users of it -- this gem's own test suite, which
- # configures through `Capybara::Screenshot.*` by design and would
- # otherwise print the notice on every run. Deliberately survives
- # {reset!}, which exists to give a single test a clean slate.
- # @return [void]
- def suppress_migration_notice!
- MUTEX.synchronize { @notice_suppressed = true }
- end
-
- # Emit a deprecation warning for +subject+, exactly once per unique
- # +subject+ per process -- preceded, the first time round, by
- # {MIGRATION_NOTICE}.
- #
- # @param subject [String] the deprecated old-namespace name being
- # referenced, e.g. "Capybara::Screenshot::Diff::ImageCompare"
- # @param replacement [String] the new-namespace name to use instead
- # @return [void]
- def warn(subject, replacement)
- return if SnapDiff.silence_deprecations?
-
- notice
-
- first_time = MUTEX.synchronize do
- @seen.key?(subject) ? false : (@seen[subject] = true)
- end
- return unless first_time
-
- Kernel.warn(message_for(subject, replacement, caller_locations(1)))
- end
-
- # @api private
- #
- # Clears the seen-set. For tests only -- lets each example assert
- # "warns once" from a clean slate instead of leaking state across
- # the suite.
- # @return [void]
- def reset!
- MUTEX.synchronize do
- @seen.clear
- @notified = false
- end
- end
-
- private
-
- def message_for(subject, replacement, locations)
- message = "[snap_diff deprecation] `#{subject}` is deprecated (constant); " \
- "use `#{replacement}` instead."
- origin = origin_for(locations)
- origin ? "#{message} (called from #{origin})" : message
- end
-
- # First frame outside the gem's lib dir, formatted "file:line";
- # nil when every frame is internal (or paths are unavailable).
- def origin_for(locations)
- (locations || []).each do |location|
- path = location.absolute_path || location.path
- next if path.nil? || path.start_with?(GEM_LIB_DIR)
-
- return "#{path}:#{location.lineno}"
- end
- nil
- end
- end
- end
-end
diff --git a/lib/snap_diff/driver.rb b/lib/snap_diff/driver.rb
deleted file mode 100644
index 7844b6f7..00000000
--- a/lib/snap_diff/driver.rb
+++ /dev/null
@@ -1,55 +0,0 @@
-# frozen_string_literal: true
-
-require "snap_diff/removal"
-
-module SnapDiff
- # Shared default behavior for image-processing drivers.
- #
- # Replaces the old +Capybara::Screenshot::Diff::Drivers::BaseDriver+
- # superclass (ADR-004 v2 step 4): concrete drivers +include Driver+
- # instead of inheriting. Method names are intentionally unchanged from
- # v1 — see dissent #4 in the v2 architecture design.
- module Driver
- PNG_EXTENSION = ".png"
-
- # Including this mixin is what makes a custom driver a driver, so it is
- # where a custom-driver author can be told that 2.1 removes the whole
- # abstraction. Scoped to drivers that are NOT the gem's own two: both
- # bundled drivers include it themselves, and warning there would fire on
- # every plain vips setup -- about code the user does not own.
- def self.included(base)
- return if base.name.to_s.start_with?("SnapDiff::")
-
- Removal.warn_once(
- :driver_mixin,
- "`include SnapDiff::Driver` (in #{base.name || base.inspect}) is REMOVED in 2.1: the " \
- "driver abstraction goes away and libvips becomes the only backend, so custom drivers " \
- "stop working. There is no replacement -- see docs/drivers.md."
- )
- end
-
- def same_dimension?(comparison)
- dimension(comparison.base_image) == dimension(comparison.new_image)
- end
-
- def height_for(image)
- image.height
- end
-
- def width_for(image)
- image.width
- end
-
- def image_area_size(image)
- width_for(image) * height_for(image)
- end
-
- def dimension(image)
- [width_for(image), height_for(image)]
- end
-
- def supports?(feature)
- respond_to?(feature)
- end
- end
-end
diff --git a/lib/snap_diff/drivers.rb b/lib/snap_diff/drivers.rb
deleted file mode 100644
index 52bfb4ad..00000000
--- a/lib/snap_diff/drivers.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-# frozen_string_literal: true
-
-require "snap_diff/removal"
-
-module SnapDiff
- # utils.rb requires THIS file at its top. Requiring it back at load time
- # made Ruby shout "circular require considered harmful" under $VERBOSE --
- # which Rake::TestTask sets by default, i.e. every standard Rails/Minitest
- # suite. Autoload breaks the cycle without narrowing the surface: nothing
- # here touches Utils until a method runs, and requiring this file still
- # leaves SnapDiff::Utils resolvable exactly as the eager require did.
- autoload :Utils, "snap_diff/utils"
-
- # Compare two images and determine if they are equal, different, or within some comparison
- # range considering color values and difference area size.
- module Drivers
- def self.for(driver_options = {})
- driver_option = driver_options.is_a?(Hash) ? driver_options.fetch(:driver, :chunky_png) : driver_options
- return driver_option unless driver_option.is_a?(Symbol)
-
- Utils.find_driver_class_for(driver_option).new
- end
-
- # @api private
- #
- # The registry itself, unannounced: driver name => driver class, filled
- # lazily by Utils.find_driver_class_for and mutated in place. The gem's
- # own reads go through HERE rather than through .loaded, so the removal
- # warning below stays a signal about USER code -- a gem that warns at
- # itself teaches people to ignore its warnings.
- def self.registry
- @registry ||= {}
- end
-
- # Canonical driver-class cache (ADR-008 step 5b, ex
- # Capybara::Screenshot::Diff::LOADED_DRIVERS): driver name => driver
- # class. Mutated in place -- including by user registration through the
- # legacy constant, which legacy_shims pins as an EAGER same-object alias
- # of this hash (a lazy copy would silently drop such registrations).
- #
- # THE documented custom-driver registration point (docs/snapdiff.md), so
- # a custom-driver author has to hear that 2.1 takes it away.
- def self.loaded
- Removal.warn_once(
- :drivers_loaded,
- "`SnapDiff::Drivers.loaded` is REMOVED in 2.1 together with the rest of the driver " \
- "abstraction (`SnapDiff::Driver`, `SnapDiff::Drivers.available`, `driver: :auto`): " \
- "libvips becomes the only backend and custom drivers are no longer supported. " \
- "See docs/drivers.md."
- )
- registry
- end
-
- # Which image drivers this process can actually load, in preference
- # order. Tries the gem first and falls back to `require`, cleaning up
- # the half-defined constant a failed native load leaves behind.
- def self.detect_available
- result = []
- begin
- result << :vips if defined?(Vips) || require("vips")
- rescue LoadError
- # vips not present
- Object.send(:remove_const, :Vips) if defined?(Vips)
- end
- begin
- result << :chunky_png if defined?(ChunkyPNG) || require("chunky_png")
- rescue LoadError
- # chunky_png not present
- Object.send(:remove_const, :ChunkyPNG) if defined?(ChunkyPNG)
- end
- result
- end
-
- # Canonical home of the detected-drivers list (3.0 readiness: it used
- # to live only on Capybara::Screenshot::Diff::AVAILABLE_DRIVERS, so
- # `require "snap_diff/drivers"` alone left .available raising
- # NameError). Detection runs HERE, at this file's load, and the legacy
- # constant is now an eager same-object alias of this one.
- AVAILABLE_DRIVERS = detect_available.freeze
-
- # The driver classes are documented names (the legacy
- # `...::Drivers::VipsDriver` path is a same-object alias of this one), so
- # naming one has to work without a prior require. Autoload rather than
- # require: naming one must not cost every process the vips/chunky_png
- # load, and Utils.find_driver_class_for still requires them explicitly.
- #
- # Gated on AVAILABLE_DRIVERS, which is why these sit BELOW it. Declaring
- # them unconditionally made `const_defined?(:VipsDriver)` true on a box
- # without ruby-vips (neither driver gem is a runtime dependency), so the
- # documented v1 pattern `Diff.driver = :vips if defined?(...VipsDriver)`
- # took the branch and then blew up on const_get. v1.12.0 loaded
- # vips_driver.rb only from find_driver_class_for, so `defined?` was nil
- # there -- this keeps that.
- autoload :ChunkyPNGDriver, "snap_diff/drivers/chunky_png_driver" if AVAILABLE_DRIVERS.include?(:chunky_png)
- autoload :VipsDriver, "snap_diff/drivers/vips_driver" if AVAILABLE_DRIVERS.include?(:vips)
-
- # Canonical read API for the list above. Reads the constant live rather
- # than caching, because the constant is the published stubbing point
- # (image_compare_test stubs it to [] to exercise the no-drivers error
- # path).
- #
- # Detection only exists because there is a choice of backend to detect;
- # 2.1 removes the choice, so it warns. The gem's own callers read
- # AVAILABLE_DRIVERS directly -- same value, same stubbing point, no
- # warning at itself.
- def self.available
- Removal.warn_once(
- :drivers_available,
- "`SnapDiff::Drivers.available` is REMOVED in 2.1: with libvips the only backend there " \
- "is nothing left to detect. Require the `ruby-vips` gem instead of branching on this " \
- "list. See docs/drivers.md."
- )
- AVAILABLE_DRIVERS
- end
- end
-end
diff --git a/lib/snap_diff/drivers/chunky_png_driver.rb b/lib/snap_diff/drivers/chunky_png_driver.rb
deleted file mode 100644
index 603967f0..00000000
--- a/lib/snap_diff/drivers/chunky_png_driver.rb
+++ /dev/null
@@ -1,298 +0,0 @@
-# frozen_string_literal: true
-
-begin
- require "chunky_png"
-rescue LoadError => e
- raise 'Required chunky_png gem is missing. Add `gem "chunky_png"` to Gemfile' if e.message.match?(/chunky_png/i)
- raise
-end
-
-require "snap_diff/driver"
-require "snap_diff/drivers"
-require "snap_diff/comparison_result"
-
-module SnapDiff
- module Drivers
- class ChunkyPNGDriver
- include SnapDiff::Driver
- include ChunkyPNG::Color
-
- def load_images(old_file_name, new_file_name)
- old_bytes, new_bytes = load_image_files(old_file_name, new_file_name)
-
- _load_images(old_bytes, new_bytes)
- end
-
- def add_black_box(image, _region)
- image
- end
-
- def find_difference_region(comparison)
- DifferenceRegionFinder.new(comparison, self).perform
- end
-
- def crop(region, i)
- i.crop(*region.to_top_left_corner_coordinates)
- end
-
- def from_file(filename_or_path)
- ChunkyPNG::Image.from_file(filename_or_path.to_s)
- end
-
- def save_image_to(image, filename)
- image.save(filename, :fast_rgba)
- end
-
- def resize_image_to(image, new_width, new_height)
- image.resample_bilinear(new_width, new_height)
- end
-
- def load_image_files(old_file_name, file_name)
- [old_file_name.binread, file_name.binread]
- end
-
- def draw_rectangles(images, region, (r, g, b), offset: 0)
- border_color = ChunkyPNG::Color.rgb(r, g, b)
- border_shadow = ChunkyPNG::Color.rgba(r, g, b, 100)
-
- images.map do |image|
- new_img = image.dup
- new_img.rect(region.left - offset, region.top - offset, region.right + offset, region.bottom + offset, border_color)
- new_img.rect(region.left, region.top, region.right, region.bottom, border_shadow)
- new_img
- end
- end
-
- def same_pixels?(comparison)
- comparison.new_image == comparison.base_image
- end
-
- private
-
- def _load_images(old_file, new_file)
- [ChunkyPNG::Image.from_blob(old_file), ChunkyPNG::Image.from_blob(new_file)]
- end
-
- class DifferenceRegionFinder
- attr_accessor :skip_area, :color_distance_limit, :shift_distance_limit
-
- def initialize(comparison, driver = nil)
- @comparison = comparison
- @driver = driver
-
- @color_distance_limit = comparison.options[:color_distance_limit]
- @shift_distance_limit = comparison.options[:shift_distance_limit]
- @skip_area = comparison.options[:skip_area]
- end
-
- def perform
- find_difference_region(@comparison)
- end
-
- def find_difference_region(comparison)
- new_image, base_image, = comparison.new_image, comparison.base_image
-
- meta = {}
- meta[:max_color_distance] = 0
- meta[:max_shift_distance] = 0 if shift_distance_limit
-
- region = find_top(base_image, new_image, cache: meta)
- region = if region.nil? || region[1].nil?
- nil
- else
- find_diff_rectangle(base_image, new_image, region, cache: meta)
- end
-
- result = ComparisonResult.new(region, meta, comparison)
-
- unless result.blank?
- meta[:max_color_distance] = meta[:max_color_distance].ceil(1) if meta[:max_color_distance]
-
- if comparison.options[:tolerance]
- meta[:difference_level] = difference_level(nil, base_image, region)
- end
- end
-
- result
- end
-
- def difference_level(_diff_mask, base_image, region)
- image_area_size = @driver.image_area_size(base_image)
- return nil if image_area_size.zero?
-
- region.size.to_f / image_area_size
- end
-
- def find_diff_rectangle(org_img, new_img, area_coordinates, cache:)
- left, top, right, bottom = find_left_right_and_top(org_img, new_img, area_coordinates, cache: cache)
- bottom = find_bottom(org_img, new_img, left, right, bottom, cache: cache)
-
- Region.from_edge_coordinates(left, top, right, bottom)
- end
-
- def find_top(old_img, new_img, cache:)
- old_img.height.times do |y|
- old_img.width.times do |x|
- return [x, y, x, y] unless same_color?(old_img, new_img, x, y, cache: cache)
- end
- end
- nil
- end
-
- def find_left_right_and_top(old_img, new_img, region, cache:)
- region = region.is_a?(Region) ? region.to_edge_coordinates : region
-
- left = region[0] || old_img.width - 1
- top = region[1]
- right = region[2] || 0
- bottom = region[3]
-
- old_img.height.times do |y|
- (0...left).find do |x|
- next if same_color?(old_img, new_img, x, y, cache: cache)
-
- top ||= y
- bottom = y
- left = x
- right = x if x > right
- x
- end
- (old_img.width - 1).step(right + 1, -1).find do |x|
- unless same_color?(old_img, new_img, x, y, cache: cache)
- bottom = y
- right = x
- end
- end
- end
-
- [left, top, right, bottom]
- end
-
- def find_bottom(old_img, new_img, left, right, bottom, cache:)
- if bottom
- (old_img.height - 1).step(bottom + 1, -1).find do |y|
- (left..right).find do |x|
- bottom = y unless same_color?(old_img, new_img, x, y, cache: cache)
- end
- end
- end
-
- bottom
- end
-
- def same_color?(old_img, new_img, x, y, cache:)
- return true if skipped_region?(x, y)
-
- color_distance =
- color_distance_at(new_img, old_img, x, y, shift_distance_limit: @shift_distance_limit)
-
- if color_distance > cache[:max_color_distance]
- cache[:max_color_distance] = color_distance
- end
-
- color_matches = color_distance == 0 ||
- (!!@color_distance_limit && @color_distance_limit > 0 && color_distance <= @color_distance_limit)
-
- return color_matches if !@shift_distance_limit || cache[:max_shift_distance] == Float::INFINITY
-
- shift_distance = (color_matches && 0) ||
- shift_distance_at(new_img, old_img, x, y, color_distance_limit: @color_distance_limit)
- if shift_distance && (cache[:max_shift_distance].nil? || shift_distance > cache[:max_shift_distance])
- cache[:max_shift_distance] = shift_distance
- end
-
- color_matches
- end
-
- def skipped_region?(x, y)
- return false unless @skip_area
-
- @skip_area.any? { |region| region.cover?(x, y) }
- end
-
- def color_distance_at(new_img, old_img, x, y, shift_distance_limit:)
- org_color = old_img[x, y]
- unless shift_distance_limit
- return ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_img[x, y])
- end
-
- start_x = [0, x - shift_distance_limit].max
- end_x = [x + shift_distance_limit, new_img.width - 1].min
- start_y = [0, y - shift_distance_limit].max
- end_y = [y + shift_distance_limit, new_img.height - 1].min
-
- min_distance = Float::INFINITY
- (start_y..end_y).each do |dy|
- (start_x..end_x).each do |dx|
- distance = ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_img[dx, dy])
- return 0 if distance == 0
- min_distance = distance if distance < min_distance
- end
- end
- min_distance
- end
-
- def shift_distance_at(new_img, old_img, x, y, color_distance_limit:)
- org_color = old_img[x, y]
- shift_distance = 0
- loop do
- bounds_breached = 0
- top_row = y - shift_distance
- if top_row >= 0 # top
- ([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
- if color_matches(new_img, org_color, dx, top_row, color_distance_limit)
- return shift_distance
- end
- end
- else
- bounds_breached += 1
- end
- if shift_distance > 0
- if (x - shift_distance) >= 0 # left
- ([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
- .each do |dy|
- if color_matches(new_img, org_color, x - shift_distance, dy, color_distance_limit)
- return shift_distance
- end
- end
- else
- bounds_breached += 1
- end
- if (y + shift_distance) < new_img.height # bottom
- ([0, x - shift_distance].max..[x + shift_distance, new_img.width - 1].min).each do |dx|
- if color_matches(new_img, org_color, dx, y + shift_distance, color_distance_limit)
- return shift_distance
- end
- end
- else
- bounds_breached += 1
- end
- if (x + shift_distance) < new_img.width # right
- ([0, top_row + 1].max..[y + shift_distance, new_img.height - 2].min)
- .each do |dy|
- if color_matches(new_img, org_color, x + shift_distance, dy, color_distance_limit)
- return shift_distance
- end
- end
- else
- bounds_breached += 1
- end
- end
- break if bounds_breached == 4
-
- shift_distance += 1
- end
- Float::INFINITY
- end
-
- def color_matches(new_img, org_color, x, y, color_distance_limit)
- new_color = new_img[x, y]
- return new_color == org_color unless color_distance_limit
-
- color_distance = ChunkyPNG::Color.euclidean_distance_rgba(org_color, new_color)
- color_distance <= color_distance_limit
- end
- end
- end
- end
-end
diff --git a/lib/snap_diff/drivers/vips_driver.rb b/lib/snap_diff/drivers/vips_driver.rb
index e9e3ec46..62648a79 100644
--- a/lib/snap_diff/drivers/vips_driver.rb
+++ b/lib/snap_diff/drivers/vips_driver.rb
@@ -7,26 +7,33 @@
raise
end
-require "snap_diff/driver"
-require "snap_diff/drivers"
require "snap_diff/comparison_result"
# Defines SnapDiff::RED_RGBA, the highlight_mask default color.
require "snap_diff/annotation_service"
module SnapDiff
module Drivers
+ # THE image backend. 2.1 removed the driver abstraction (the
+ # +SnapDiff::Driver+ mixin, the +SnapDiff::Drivers+ registry and
+ # driver: :auto selection) along with the chunky_png driver, so
+ # this class is no longer one of several -- +ruby-vips+ is a runtime
+ # dependency and every comparison runs through here. The dimension
+ # helpers below came from the mixin; with one includer they live here.
+ #
+ # +Drivers+ survives only as the namespace this class has always been
+ # published under (docs/drivers.md), not as a registry.
class VipsDriver
- include SnapDiff::Driver
+ PNG_EXTENSION = ".png"
# libvips caches loader operations keyed on filename + mtime, and mtime
# has ONE-SECOND resolution -- so overwriting a path and re-reading it
# within the same second hands back the PREVIOUS image. This gem does
- # exactly that: the screenshoter writes `.png`,
- # `checkout_base_screenshot` writes `.base.png` from VCS, and the
- # comparison then reads both.
+ # exactly that: the screenshoter writes `.png`, `checkout_base_screenshot`
+ # writes `.base.png` from VCS, and the comparison then reads both.
#
# `revalidate: true` tells the loader to skip the cached result (libvips
- # 8.15+).
+ # 8.15+). It was latent until 2.1 because comparisons built without an
+ # explicit driver defaulted to chunky_png, which always re-read the file.
REVALIDATE = Vips.at_least_libvips?(8, 15) ? {revalidate: true}.freeze : {}.freeze
def find_difference_region(comparison)
@@ -115,6 +122,30 @@ def same_pixels?(comparison)
(comparison.new_image == comparison.base_image).min == 255
end
+ # --- ex-SnapDiff::Driver mixin --------------------------------------
+ # Dimension helpers, unchanged. They were shared because there were two
+ # drivers; there is one.
+
+ def same_dimension?(comparison)
+ dimension(comparison.base_image) == dimension(comparison.new_image)
+ end
+
+ def height_for(image)
+ image.height
+ end
+
+ def width_for(image)
+ image.width
+ end
+
+ def image_area_size(image)
+ width_for(image) * height_for(image)
+ end
+
+ def dimension(image)
+ [width_for(image), height_for(image)]
+ end
+
def merge(new_image, base_image)
base_image.composite2(new_image, :over)
end
diff --git a/lib/snap_diff/dsl.rb b/lib/snap_diff/dsl.rb
index 2695c8c7..2580e89b 100644
--- a/lib/snap_diff/dsl.rb
+++ b/lib/snap_diff/dsl.rb
@@ -7,16 +7,10 @@
# which of those paths the user picked.
require "snap_diff"
-# Must NOT require "capybara_screenshot_diff": that would cycle back here via
-# this file's old-path forwarder. Nothing from the v1 trees is required here
-# at all (3.0 readiness): the three requires below used to point at their
-# capybara/screenshot/diff/* forwarders, which made this unit depend on the
-# compatibility tree it is meant to replace.
# DSL includes Capybara::DSL directly below, so it needs the base gem
# loaded regardless of what pulled this file in.
require "capybara/dsl"
require "snap_diff/config"
-require "snap_diff/drivers"
require "snap_diff/comparison"
require "snap_diff/screenshot_matcher"
require_relative "screenshot_namer"
@@ -55,15 +49,13 @@ def screenshot_group(name)
# Whether to validate the screenshot immediately or delay validation.
# @option options [Array] :crop [left, top, right, bottom] Edge coordinates to crop the screenshot to.
# @option options [Array>] :skip_area Array of [left, top, right, bottom] edge coordinates to ignore.
- # @option options [Numeric] :tolerance (0.001 for :vips driver) Color tolerance for comparison.
+ # @option options [Numeric] :tolerance (0.001) Color tolerance for comparison.
# Represents the maximum allowed ratio of different pixels (0.0-1.0 scale).
# @option options [Numeric] :color_distance_limit Maximum allowed color distance between pixels.
# Uses Euclidean RGBA distance (0-510 scale). Mutually exclusive with :perceptual_threshold.
# @option options [Numeric] :perceptual_threshold Maximum perceptual color difference (CIE dE00).
- # Uses human perception-based scale (0-100+). VIPS only. Takes priority over :color_distance_limit if both set.
- # @option options [Numeric] :shift_distance_limit Maximum allowed shift distance for pixels.
+ # Uses human perception-based scale (0-100+). Takes priority over :color_distance_limit if both set.
# @option options [Numeric] :area_size_limit Maximum allowed difference area size in pixels.
- # @option options [Symbol] :driver (:auto) The image processing driver to use (:auto, :chunky_png, :vips).
# @return [Boolean] True if the screenshot was successfully captured and processed.
# @raise [SnapDiff::ExpectationNotMet] If comparison fails and immediate validation is enabled.
# @raise [SnapDiff::UnstableImage] If the image comparison is unstable.
diff --git a/lib/snap_diff/errors.rb b/lib/snap_diff/errors.rb
index 56edfd12..803bf628 100644
--- a/lib/snap_diff/errors.rb
+++ b/lib/snap_diff/errors.rb
@@ -2,12 +2,8 @@
require "snap_diff/error_with_filtered_backtrace"
-# ADR-008 step 2: the gem's error classes live under SnapDiff. The old
-# CapybaraScreenshotDiff names (capybara_screenshot_diff.rb) are EAGER
-# same-object aliases of these classes -- deliberately not const_missing
-# shims, because rescue clauses and defined?/const_defined? feature
-# detection in adopter code must keep behaving exactly as before
-# (const_defined? never triggers const_missing).
+# ADR-008 step 2: the gem's error classes live under SnapDiff. 2.1 deleted
+# the v1 aliases of them, so these names are the only ones.
#
# Error is the catch-all docs/snapdiff.md advertises: EVERY error this gem
# raises inherits it, so `rescue SnapDiff::Error` really does catch them all
diff --git a/lib/snap_diff/image_preprocessor.rb b/lib/snap_diff/image_preprocessor.rb
index 7e71dc20..73496fd5 100644
--- a/lib/snap_diff/image_preprocessor.rb
+++ b/lib/snap_diff/image_preprocessor.rb
@@ -28,10 +28,10 @@ def process_comparison(comparison)
private
- def process_image(image, path)
+ def process_image(image, _path)
result = image
result = apply_skip_area(result) if skip_area
- result = apply_median_filter(result, path) if median_filter_window_size
+ result = apply_median_filter(result) if median_filter_window_size
result
end
@@ -41,20 +41,11 @@ def apply_skip_area(image)
end
end
- def apply_median_filter(image, path)
- if driver.supports?(:filter_image_with_median)
- driver.filter_image_with_median(image, median_filter_window_size)
- else
- warn_about_skipped_median_filter(path)
- image
- end
- end
-
- def warn_about_skipped_median_filter(path)
- warn(
- "[capybara-screenshot-diff] Median filter has been skipped for #{path} " \
- "because it is not supported by #{driver.class}"
- )
+ # Unconditional since 2.1: libvips is the only backend and it implements
+ # the filter. The `driver.supports?` guard (and the warning it fell back
+ # to) existed for chunky_png, which did not.
+ def apply_median_filter(image)
+ driver.filter_image_with_median(image, median_filter_window_size)
end
def skip_area
diff --git a/lib/snap_diff/integrations/minitest.rb b/lib/snap_diff/integrations/minitest.rb
index baadf30c..c4d27ea5 100644
--- a/lib/snap_diff/integrations/minitest.rb
+++ b/lib/snap_diff/integrations/minitest.rb
@@ -8,17 +8,6 @@
require "snap_diff/screenshot_assertion"
require "snap_diff/reporting"
-used_deprecated_entrypoint = caller.any? do |path|
- path.include?("capybara-screenshot-diff.rb") || path.include?("capybara/screenshot/diff.rb")
-end
-
-if used_deprecated_entrypoint
- warn <<~MSG
- [DEPRECATION] The default activation of `capybara_screenshot_diff/minitest` will be removed.
- Please `require "capybara_screenshot_diff/minitest"` explicitly.
- MSG
-end
-
module SnapDiff
module Minitest
module Assertions
diff --git a/lib/snap_diff/legacy_shims.rb b/lib/snap_diff/legacy_shims.rb
deleted file mode 100644
index fe708489..00000000
--- a/lib/snap_diff/legacy_shims.rb
+++ /dev/null
@@ -1,366 +0,0 @@
-# frozen_string_literal: true
-
-require "snap_diff/comparison"
-require "snap_diff/config"
-require "snap_diff/deprecation"
-require "snap_diff/drivers"
-require "snap_diff/errors"
-require "snap_diff/os"
-require "snap_diff/reporters/default"
-require "snap_diff/version"
-
-# THE v1 compatibility surface, in one file -- and the whole of it that is
-# code. lib/capybara* is alias-only by contract
-# (legacy_tree_is_alias_only_test.rb) and the canonical core names nothing
-# from it (core_tree_has_no_legacy_deps_test.rb), so this file plus those
-# trees is exactly what 3.0 deletes.
-#
-# Three things live here:
-# 1. the const_missing forwarders for the pre-v2 namespaces (below);
-# 2. CONFIG_MAPPING -- the old mattr_accessor surface, generated as thin
-# delegators onto SnapDiff.config (which owns the storage);
-# 3. the derived/config forwarders that used to sit in config_legacy.rb
-# (Screenshot.active?, Diff.configure, SnapDiff.start, ...).
-#
-# 2 and 3 moved here so `require "snap_diff"` can keep offering the full v1
-# surface -- as it always has -- without the core requiring anything from
-# lib/capybara/.
-#
-# const_missing-based forwarders for the pre-v2 namespaces. Every old-name
-# lookup below resolves -- lazily -- to the exact
-# same object as its SnapDiff:: replacement (identity pinned by
-# test/unit/namespace_forwarding_test.rb) and emits a deprecation warning,
-# once per constant per process, silenceable via
-# SnapDiff.silence_deprecations or SNAP_DIFF_SILENCE_DEPRECATIONS=1.
-#
-# Deliberately eager-and-silent exceptions (plain constants assigned BELOW,
-# never warn individually -- the once-per-process migration notice still
-# fires for the paths that CAN be hooked; see Deprecation.notice):
-#
-# - Capybara::Screenshot::Os: an advertised entry-point constant probed with
-# Object.const_defined? by support_load_probe_test.rb -- const_defined?
-# never triggers const_missing, so a lazy shim would break that contract.
-# - Capybara::Screenshot::Diff::VERSION, and ::Comparison (the images
-# struct): documented user-facing names that adopters feature-detect with
-# defined?/const_defined?.
-# - Capybara::Screenshot::Diff::Reporters::Default (a documented subclassing
-# extension point): same reasoning.
-# - The CapybaraScreenshotDiff error classes: rescue-by-old-name and
-# defined? feature detection must keep behaving exactly as before.
-# - Drivers::ChunkyPNGDriver / Drivers::VipsDriver: real constants on the
-# shared SnapDiff::Drivers module (the Drivers alias is same-object by
-# contract), so const_missing can never fire for the leaf names;
-# resolving them through the old path still warns for ...::Drivers. They
-# are `autoload`ed there, so naming one loads it.
-# - Diff::LOADED_DRIVERS: user code registers custom drivers by mutating
-# this hash in place, so it must be the exact same object as the
-# canonical SnapDiff::Drivers.loaded -- a lazy warn-once shim could not
-# keep a mutable alias, and warning on a supported registration surface
-# would be noise. Assigned eagerly below.
-# - Diff::AVAILABLE_DRIVERS: stays a real constant, aliased BELOW from the
-# canonical SnapDiff::Drivers::AVAILABLE_DRIVERS (same object;
-# SnapDiff::Drivers.available is the canonical reader, and its constant is
-# the stubbing point -- stubbing this alias only rebinds the alias).
-#
-# All of them are assigned HERE rather than in their own forwarder files
-# under lib/capybara*. Those forwarders are loaded only by the LEGACY entry
-# points, so a partially migrated app -- one that swapped its `require` line
-# for a canonical `snap_diff*` one first, exactly as UPGRADING.md tells it
-# to, and has not renamed its constants yet -- lost every one of them and
-# died on `uninitialized constant Capybara::Screenshot::Os`. This file is
-# required by every entry point, canonical and legacy, so it is the only
-# place the eager exceptions can actually be eager.
-#
-# CapybaraScreenshotDiff::DSL and ::Minitest::Assertions are the two that
-# CANNOT be eager here: snap_diff/dsl requires "snap_diff" (which requires
-# this file), and snap_diff/integrations/minitest pulls in the minitest gem,
-# which no canonical entry point should force on a process. They are mapped
-# lazily below instead, and stay eager under the legacy entry points that
-# load their forwarder files.
-
-# The v1 namespaces, predefined empty so CONFIG_MAPPING can name them at
-# class-body eval time. Everything below reopens them.
-module Capybara
- module Screenshot
- module Diff
- end
- end
-end
-
-module SnapDiff
- # @api private
- module LegacyShims
- # Installs a warn-then-forward const_missing on +namespace+.
- #
- # @param namespace [Module] the old namespace to hook
- # @param old_prefix [String] how the old constant path reads to a human
- # @param mapping [Hash{Symbol => String}] old leaf name => new full name
- def self.install(namespace, old_prefix, mapping)
- namespace.define_singleton_method(:const_missing) do |name|
- target = mapping[name]
- return super(name) unless target
-
- Deprecation.warn("#{old_prefix}::#{name}", target)
- LegacyShims.resolve("#{old_prefix}::#{name}", target)
- end
- end
-
- # The handful of replacements whose file name does not follow the gem's
- # own convention (SnapDiff::AreaCalculator -> snap_diff/area_calculator).
- REQUIRE_PATHS = {
- "SnapDiff::Minitest::Assertions" => "snap_diff/integrations/minitest"
- }.freeze
-
- # Resolving an old name has to LOAD the replacement, not merely name it.
- # The v1 entry points required the whole gem, so v1 code could say
- # `Capybara::Screenshot::Diff::Utils` with nothing else required; the
- # canonical entry points are lean, so the shim used to resolve its
- # mapping and then die on a bare "uninitialized constant SnapDiff::Utils"
- # -- an internal name the reader has no way to act on.
- def self.resolve(old_name, target)
- require_unit(target) unless Object.const_defined?(target)
- Object.const_get(target)
- rescue NameError
- # Deliberately does NOT advise "reference #{target} directly": we just
- # failed to load it, so that name does not exist either.
- raise NameError, "`#{old_name}` maps to `#{target}`, which this process cannot load. " \
- "See docs/UPGRADING.md for the v1 -> SnapDiff name map."
- end
-
- def self.require_unit(target)
- require(REQUIRE_PATHS[target] || target
- .gsub("::", "/")
- .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
- .gsub(/([a-z\d])([A-Z])/, '\1_\2')
- .downcase)
- rescue LoadError
- # No file of its own -- the name lives inside another unit
- # (SnapDiff::RED_RGBA, ::BacktraceFilter). The const_get above decides
- # whether it is already loaded.
- end
-
- # config attr name => [legacy module, legacy accessor name].
- # The keys are exactly SnapDiff::Config::SETTINGS; this hash only says
- # which of the two legacy holders each one used to hang off, and under
- # what name (only +screenshot_enabled+ differs -- see Config::SETTINGS).
- CONFIG_MAPPING = {
- # Capybara::Screenshot
- add_driver_path: [Capybara::Screenshot, :add_driver_path],
- add_os_path: [Capybara::Screenshot, :add_os_path],
- blur_active_element: [Capybara::Screenshot, :blur_active_element],
- screenshot_enabled: [Capybara::Screenshot, :enabled],
- hide_caret: [Capybara::Screenshot, :hide_caret],
- disable_animations: [Capybara::Screenshot, :disable_animations],
- root: [Capybara::Screenshot, :root],
- stability_time_limit: [Capybara::Screenshot, :stability_time_limit],
- window_size: [Capybara::Screenshot, :window_size],
- save_path: [Capybara::Screenshot, :save_path],
- use_lfs: [Capybara::Screenshot, :use_lfs],
- screenshot_format: [Capybara::Screenshot, :screenshot_format],
- capybara_screenshot_options: [Capybara::Screenshot, :capybara_screenshot_options],
- # Capybara::Screenshot::Diff
- delayed: [Capybara::Screenshot::Diff, :delayed],
- area_size_limit: [Capybara::Screenshot::Diff, :area_size_limit],
- fail_if_new: [Capybara::Screenshot::Diff, :fail_if_new],
- pending_if_new: [Capybara::Screenshot::Diff, :pending_if_new],
- fail_on_difference: [Capybara::Screenshot::Diff, :fail_on_difference],
- color_distance_limit: [Capybara::Screenshot::Diff, :color_distance_limit],
- enabled: [Capybara::Screenshot::Diff, :enabled],
- shift_distance_limit: [Capybara::Screenshot::Diff, :shift_distance_limit],
- skip_area: [Capybara::Screenshot::Diff, :skip_area],
- driver: [Capybara::Screenshot::Diff, :driver],
- tolerance: [Capybara::Screenshot::Diff, :tolerance],
- perceptual_threshold: [Capybara::Screenshot::Diff, :perceptual_threshold],
- screenshoter: [Capybara::Screenshot::Diff, :screenshoter],
- manager: [Capybara::Screenshot::Diff, :manager]
- }.freeze
-
- # Installs the old mattr_accessor surface onto the legacy modules,
- # delegating to the single storage in SnapDiff.config. mattr_accessor
- # used to define both singleton and instance accessors (the instance
- # ones are what `include Capybara::Screenshot::Diff` picks up), so both
- # are installed. root keeps its historical asymmetry -- readable
- # everywhere, writable only at module level (it was mattr_reader plus a
- # custom module-level writer) -- with the Pathname coercion living in
- # Config#root=.
- def self.install_config_accessors
- CONFIG_MAPPING.each do |name, (mod, mattr)|
- [mod, mod.singleton_class].each do |target|
- target.define_method(mattr) do
- Deprecation.notice
- SnapDiff.config.public_send(name)
- end
- next if name == :root && target == mod
-
- target.define_method(:"#{mattr}=") do |value|
- Deprecation.notice
- SnapDiff.config.public_send(:"#{name}=", value)
- end
- end
- end
- end
-
- # `include Capybara::Screenshot::Diff` is the third way into the v1
- # surface (it picks up the instance-level accessors installed above) and
- # resolves no deprecated constant of its own, so it needs its own hook
- # for the once-per-process notice.
- def self.install_include_notice(mod)
- mod.define_singleton_method(:included) do |base|
- Deprecation.notice
- super(base)
- end
- end
- end
-end
-
-SnapDiff::LegacyShims.install_config_accessors
-SnapDiff::LegacyShims.install_include_notice(Capybara::Screenshot)
-SnapDiff::LegacyShims.install_include_notice(Capybara::Screenshot::Diff)
-
-module SnapDiff
- # v1-style configuration: yields the two legacy accessor holders
- # (+Capybara::Screenshot+, +Capybara::Screenshot::Diff+) exactly as
- # +Capybara::Screenshot::Diff.configure+ always has -- and, since ADR-008
- # step 7b, this is where that yield actually happens; Diff.configure
- # forwards here. Both names stay identical in call shape.
- #
- # SnapDiff.start do |screenshot, diff|
- # screenshot.window_size = [1280, 1024]
- # diff.tolerance = 0.0005
- # end
- #
- # Defined in this file, not snap_diff.rb, because the two holders it
- # yields ARE the v1 surface: it cannot outlive them. The consolidated
- # shape, SnapDiff.configure, is the canonical one and lives in the core.
- def self.start
- yield Capybara::Screenshot, Capybara::Screenshot::Diff
- end
-end
-
-module Capybara
- module Screenshot
- # EAGER same-object alias (see header): the only place it can be eager
- # for a canonical-only require, which is what a half-migrated app has.
- Os = SnapDiff::Os
-
- # Derived config, ex config_legacy.rb: one-line forwarders onto the
- # canonical implementations in SnapDiff::Config (ADR-008 step 7b).
- class << self
- def active?
- SnapDiff.config.active?
- end
-
- def screenshot_area
- SnapDiff.config.screenshot_area
- end
-
- def screenshot_area_abs
- SnapDiff.config.screenshot_area_abs
- end
- end
-
- module Diff
- # EAGER same-object aliases of canonical values (see header for why
- # each one is eager rather than a warn-once const_missing shim).
- # .registry, not .loaded: this alias is assigned at load time by the
- # gem itself, and .loaded announces its own 2.1 removal. Same object
- # either way -- which is the whole point of the alias.
- LOADED_DRIVERS = SnapDiff::Drivers.registry
- AVAILABLE_DRIVERS = SnapDiff::Drivers::AVAILABLE_DRIVERS
- Comparison = SnapDiff::Comparison::Images
- VERSION = SnapDiff::VERSION
-
- module Reporters
- Default = SnapDiff::Reporters::Default
- end
-
- # Configure screenshot and diff settings in one block.
- #
- # Capybara::Screenshot::Diff.configure do |screenshot, diff|
- # screenshot.window_size = [1280, 1024]
- # screenshot.stability_time_limit = 1
- # diff.driver = :vips
- # diff.tolerance = 0.0005
- # end
- # The bare `yield` (rather than an explicit &block) keeps this
- # method's published arity byte-identical to what it always had.
- def self.configure
- SnapDiff.start { |screenshot, diff| yield screenshot, diff }
- end
-
- def self.compare(baseline_path, current_path, **options)
- SnapDiff.compare(baseline_path, current_path, **options)
- end
-
- def self.default_options
- SnapDiff::Deprecation.notice
- SnapDiff.config.default_options
- end
- end
- end
-end
-
-module CapybaraScreenshotDiff
- module Reporters
- end
-
- # Predefined so the mapping below has a namespace to hang const_missing
- # on; capybara_screenshot_diff/minitest reopens it with the eager alias.
- module Minitest
- end
-
- # EAGER same-object aliases (see header): rescue-by-old-name and
- # defined?/const_defined? feature detection must behave as they always
- # have, under canonical and legacy requires alike.
- CapybaraScreenshotDiffError = SnapDiff::Error
- ExpectationNotMet = SnapDiff::ExpectationNotMet
- UnstableImage = SnapDiff::UnstableImage
- WindowSizeMismatchError = SnapDiff::WindowSizeMismatchError
-end
-
-SnapDiff::LegacyShims.install(Capybara::Screenshot, "Capybara::Screenshot", {
- BrowserHelpers: "SnapDiff::BrowserHelpers",
- Screenshoter: "SnapDiff::Screenshoter"
-}.freeze)
-
-SnapDiff::LegacyShims.install(Capybara::Screenshot::Diff, "Capybara::Screenshot::Diff", {
- Vcs: "SnapDiff::Vcs",
- StableScreenshoter: "SnapDiff::StableScreenshoter",
- ImagePreprocessor: "SnapDiff::ImagePreprocessor",
- AreaCalculator: "SnapDiff::AreaCalculator",
- AnnotationService: "SnapDiff::AnnotationService",
- Utils: "SnapDiff::Utils",
- ScreenshotMatcher: "SnapDiff::ScreenshotMatcher",
- Drivers: "SnapDiff::Drivers",
- ImageCompare: "SnapDiff::Comparison",
- Difference: "SnapDiff::ComparisonResult"
-}.freeze)
-
-SnapDiff::LegacyShims.install(CapybaraScreenshotDiff, "CapybaraScreenshotDiff", {
- RED_RGBA: "SnapDiff::RED_RGBA",
- ORANGE_RGBA: "SnapDiff::ORANGE_RGBA",
- SnapManager: "SnapDiff::SnapManager",
- Snap: "SnapDiff::Snap",
- ScreenshotNamer: "SnapDiff::ScreenshotNamer",
- AttemptsReporter: "SnapDiff::AttemptsReporter",
- BacktraceFilter: "SnapDiff::BacktraceFilter",
- ErrorWithFilteredBacktrace: "SnapDiff::ErrorWithFilteredBacktrace",
- ScreenshotAssertion: "SnapDiff::ScreenshotAssertion",
- AssertionRegistry: "SnapDiff::AssertionRegistry",
- DSL: "SnapDiff::DSL"
-}.freeze)
-
-SnapDiff::LegacyShims.install(CapybaraScreenshotDiff::Reporters, "CapybaraScreenshotDiff::Reporters", {
- HTML: "SnapDiff::Reporters::HTML"
-}.freeze)
-
-SnapDiff::LegacyShims.install(CapybaraScreenshotDiff::Minitest, "CapybaraScreenshotDiff::Minitest", {
- Assertions: "SnapDiff::Minitest::Assertions"
-}.freeze)
-
-# BaseDriver dissolved into the SnapDiff::Driver mixin; the Drivers alias is
-# same-object, so the hook has to live on SnapDiff::Drivers itself.
-# `class MyDriver < BaseDriver` becomes `include SnapDiff::Driver`.
-SnapDiff::LegacyShims.install(SnapDiff::Drivers, "Capybara::Screenshot::Diff::Drivers", {
- BaseDriver: "SnapDiff::Driver"
-}.freeze)
diff --git a/lib/snap_diff/removal.rb b/lib/snap_diff/removal.rb
deleted file mode 100644
index 1b22d7a5..00000000
--- a/lib/snap_diff/removal.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-# frozen_string_literal: true
-
-module SnapDiff
- # @api private
- #
- # Announces what 2.1 REMOVES, from the 2.0 line that still supports it:
- # the chunky_png driver, +shift_distance_limit+ (chunky-only, it dies with
- # it) and the driver abstraction (+SnapDiff::Driver+,
- # +SnapDiff::Drivers.loaded+ / +.available+, driver: :auto ) --
- # libvips becomes the only backend. 2.0 is the transitional release: the
- # contract is published before it is enforced.
- #
- # Same shape and same silencing switches as {SnapDiff::Deprecation}, which
- # announces the other half (the v1 namespaces), but a file of its own: the
- # legacy shims and their deprecation channel are themselves part of what is
- # removed, while the call sites here -- utils, config, drivers -- are core
- # files that outlive them, so they cannot depend on a doomed file. That is
- # also why {SnapDiff.silence_deprecations} lives HERE rather than in
- # deprecation.rb: it is the one switch that silences both halves.
- #
- # Deliberately not a warn-per-call channel: one line per subject per
- # process is an actionable signal, N lines per comparison is noise people
- # learn to filter out.
- module Removal
- # Everything under lib/ is "the gem"; the first caller frame outside it
- # is the user code that touched the doomed API.
- GEM_LIB_DIR = File.expand_path("..", __dir__) + File::SEPARATOR
-
- # Appended to every message, so the individual messages can stay about
- # the thing being removed.
- SILENCE_HINT =
- "Silence with `SnapDiff.silence_deprecations = true` or " \
- "SNAP_DIFF_SILENCE_DEPRECATIONS=1. (shown once per process)"
-
- # The one message with two call sites -- the setting's writer (Config)
- # and the per-comparison option (Comparison) -- so it lives here rather
- # than in either of them. One subject, one warning, whichever fires.
- SHIFT_DISTANCE_LIMIT_REMOVED =
- "`shift_distance_limit` is REMOVED in 2.1: it is implemented only by the chunky_png " \
- "driver, which is removed with it. libvips has no shift-distance comparison -- drop the " \
- "option and tune `tolerance` / `color_distance_limit` instead. See docs/configuration.md."
-
- MUTEX = Mutex.new
- @seen = {}
- @suppressed = false
-
- class << self
- # Emit +message+ once per +subject+ per process, via Kernel#warn (so
- # anything hooking +Warning.warn+ sees it like any other Ruby warning).
- #
- # @param subject [Symbol] dedup key -- the doomed API, not the call site
- # @param message [String] what is removed, when, and what to do instead
- # @return [void]
- def warn_once(subject, message)
- return if @suppressed || SnapDiff.silence_deprecations?
-
- first_time = MUTEX.synchronize { @seen.key?(subject) ? false : (@seen[subject] = true) }
- return unless first_time
-
- Kernel.warn(with_origin("[snap_diff deprecation] #{message} #{SILENCE_HINT}", caller_locations(1)))
- end
-
- # @api private
- #
- # Silences these warnings for the rest of the process, without touching
- # the v1-namespace ones. For hosts that exercise the doomed APIs BY
- # DESIGN rather than depending on them -- this gem's own suite runs the
- # whole comparison matrix on chunky_png and sets shift_distance_limit,
- # and its test_helper raises on any deprecation output.
- # @return [void]
- def suppress!
- MUTEX.synchronize { @suppressed = true }
- end
-
- private
-
- def with_origin(message, locations)
- origin = origin_for(locations)
- origin ? "#{message} (called from #{origin})" : message
- end
-
- # First frame outside the gem's lib dir, formatted "file:line"; nil
- # when every frame is internal (or paths are unavailable).
- def origin_for(locations)
- (locations || []).each do |location|
- path = location.absolute_path || location.path
- next if path.nil? || path.start_with?(GEM_LIB_DIR)
-
- return "#{path}:#{location.lineno}"
- end
- nil
- end
- end
- end
-
- class << self
- # @api private
- attr_accessor :silence_deprecations
-
- # @api private
- #
- # @return [Boolean] true if deprecation warnings should be suppressed,
- # either via the {silence_deprecations} accessor or the
- # SNAP_DIFF_SILENCE_DEPRECATIONS env var (truthy = "1"/"true").
- def silence_deprecations?
- !!silence_deprecations || truthy_env?(ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"])
- end
-
- private
-
- def truthy_env?(value)
- %w[1 true].include?(value.to_s.downcase)
- end
- end
-end
diff --git a/lib/snap_diff/reporting.rb b/lib/snap_diff/reporting.rb
index 3f3096a4..244c59f1 100644
--- a/lib/snap_diff/reporting.rb
+++ b/lib/snap_diff/reporting.rb
@@ -7,9 +7,7 @@ module SnapDiff
#
# Deliberately separate from the per-test session lifecycle
# (SnapDiff.session): reporters outlive any single test, the session does
- # not. CapybaraScreenshotDiff keeps its public
- # reporters/reporters_mutex/finalize_reporters! methods as thin shims
- # over this module.
+ # not.
module Reporting
@reporters = []
@mutex = Mutex.new
diff --git a/lib/snap_diff/screenshot_assertion.rb b/lib/snap_diff/screenshot_assertion.rb
index 574aa0ed..9e3f3ca0 100644
--- a/lib/snap_diff/screenshot_assertion.rb
+++ b/lib/snap_diff/screenshot_assertion.rb
@@ -9,8 +9,7 @@ module SnapDiff
#
# The canonical home of the per-test session: the AssertionRegistry
# holding the assertions and new-screenshot names of the test running
- # here. CapybaraScreenshotDiff.registry / .reset /
- # .pending_screenshots_message are thin forwarders over these.
+ # here.
#
# Note: Thread.current[] is *fiber*-local, so a session is per fiber, not
# per thread. That is pre-existing, documented behaviour (issue #217);
diff --git a/lib/snap_diff/screenshot_matcher.rb b/lib/snap_diff/screenshot_matcher.rb
index 2dcf0054..09760446 100644
--- a/lib/snap_diff/screenshot_matcher.rb
+++ b/lib/snap_diff/screenshot_matcher.rb
@@ -61,7 +61,6 @@ def prepare_screenshot_options
driver_options[:crop] = area_calculator.calculate_crop
driver_options[:skip_area] = area_calculator.calculate_skip_area
- driver_options[:driver] = SnapDiff::Drivers.for(driver_options[:driver])
end
def check_base_screenshot
diff --git a/lib/snap_diff/screenshoter.rb b/lib/snap_diff/screenshoter.rb
index 13cc16bc..c61374e5 100644
--- a/lib/snap_diff/screenshoter.rb
+++ b/lib/snap_diff/screenshoter.rb
@@ -4,16 +4,18 @@
require_relative "os"
require_relative "browser_helpers"
+require_relative "drivers/vips_driver"
module SnapDiff
class Screenshoter
attr_reader :capture_options, :driver
# @param capture_options [Hash] Options for capturing (window_size, wait, etc.)
- # @param comparison_options [Hash] Options for image comparison (driver, tolerance, etc.)
- def initialize(capture_options, comparison_options = {})
+ # @param _comparison_options [Hash] Ignored since 2.1 removed driver
+ # selection; kept so the two-argument call sites stay unchanged.
+ def initialize(capture_options, _comparison_options = {})
@capture_options = capture_options
- @driver = SnapDiff::Drivers.for(comparison_options)
+ @driver = SnapDiff::Drivers::VipsDriver.new
end
def crop
diff --git a/lib/snap_diff/utils.rb b/lib/snap_diff/utils.rb
deleted file mode 100644
index 48f6829e..00000000
--- a/lib/snap_diff/utils.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require "snap_diff/drivers"
-require "snap_diff/removal"
-
-module SnapDiff
- module Utils
- # THE selection funnel. Every surface that picks a driver ends up here --
- # `driver: :chunky_png` per comparison, `SnapDiff.config.driver =`, the
- # legacy `Diff.driver =` (a delegator onto the same storage), and `:auto`
- # -- so this is the one place the chunky_png removal has to be announced
- # from. Warned before the registry lookup, not inside it: the cache is a
- # `||=`, so a hook there would fire only for the first comparison of a
- # process that happened to miss.
- CHUNKY_PNG_REMOVED =
- "The chunky_png driver is REMOVED in 2.1, when libvips (the `ruby-vips` gem) becomes " \
- "required. Install ruby-vips and drop `driver: :chunky_png`. See docs/drivers.md."
-
- # The case that matters most: nobody asked for this driver, so the
- # warning has to say why they are on it.
- CHUNKY_PNG_AUTO_REMOVED =
- "`driver: :auto` selected chunky_png because libvips is not available in this process. " \
- "The chunky_png driver is REMOVED in 2.1, when libvips (the `ruby-vips` gem) becomes " \
- "required -- install it now, or this setup stops comparing on 2.1. See docs/drivers.md."
-
- # Detection itself lives on Drivers now (its canonical home -- so that
- # `require "snap_diff/drivers"` standalone can answer .available); this
- # keeps the documented Utils name working. One-way: Drivers never calls
- # back here at load time, so requiring either file first is safe.
- def self.detect_available_drivers
- Drivers.detect_available
- end
-
- def self.find_driver_class_for(driver)
- if driver == :auto
- # Drivers::AVAILABLE_DRIVERS, not Drivers.available: same value and
- # the same stubbing point, without the gem tripping .available's own
- # removal warning on every comparison.
- driver = Drivers::AVAILABLE_DRIVERS.first
- Removal.warn_once(:chunky_png_auto, CHUNKY_PNG_AUTO_REMOVED) if driver == :chunky_png
- elsif driver == :chunky_png
- Removal.warn_once(:chunky_png, CHUNKY_PNG_REMOVED)
- end
-
- Drivers.registry[driver] ||=
- case driver
- when :chunky_png
- require "snap_diff/drivers/chunky_png_driver"
- SnapDiff::Drivers::ChunkyPNGDriver
- when :vips
- require "snap_diff/drivers/vips_driver"
- SnapDiff::Drivers::VipsDriver
- else
- fail "Wrong adapter #{driver.inspect}. Available adapters: #{Drivers::AVAILABLE_DRIVERS.inspect}"
- end
- end
- end
-end
diff --git a/scripts/generate_sample_report.rb b/scripts/generate_sample_report.rb
index 9b9245e1..5d77126d 100644
--- a/scripts/generate_sample_report.rb
+++ b/scripts/generate_sample_report.rb
@@ -4,14 +4,12 @@
# Uses absolute file:// paths so images load when opened directly in a browser.
require "bundler/setup"
-require "capybara_screenshot_diff"
-require "capybara_screenshot_diff/minitest"
-require "capybara/screenshot/diff"
-require "capybara_screenshot_diff/reporters/html"
+require "snap_diff/integrations/minitest"
+require "snap_diff/reporters/html"
-output_path = CapybaraScreenshotDiff::Reporters::HTML.default_output_path
+output_path = SnapDiff::Reporters::HTML.default_output_path
-# Build real comparisons using the gem's own ImageCompare.
+# Build real comparisons using the gem's own Comparison.
# Each pair gets a unique copy of the base image to avoid annotation file conflicts.
pairs = [
{name: "islands-map", base: "a", new: "b"},
@@ -20,7 +18,7 @@
]
embed = ARGV.include?("--embed") || !!ENV["CI"]
-reporter = CapybaraScreenshotDiff::Reporters::HTML.new(output_path: output_path, embed_images: embed)
+reporter = SnapDiff::Reporters::HTML.new(output_path: output_path, embed_images: embed)
fixtures = File.expand_path("../test/fixtures/images", __dir__)
tmp_dir = File.expand_path("../tmp/sample_images", __dir__)
FileUtils.mkdir_p(tmp_dir)
@@ -32,17 +30,17 @@
FileUtils.cp("#{fixtures}/#{pair[:base]}.png", base_copy)
FileUtils.cp("#{fixtures}/#{pair[:new]}.png", new_copy)
- compare = Capybara::Screenshot::Diff::ImageCompare.new(new_copy, base_copy, driver: :vips)
+ compare = SnapDiff::Comparison.new(new_copy, base_copy)
compare.processed
- CapybaraScreenshotDiff::ScreenshotAssertion.new(pair[:name]).tap { |a| a.compare = compare }
+ SnapDiff::ScreenshotAssertion.new(pair[:name]).tap { |a| a.compare = compare }
end
# Add passing assertions (identical images = no difference)
passing = %w[dashboard settings profile users].map do |name|
- compare = Capybara::Screenshot::Diff::ImageCompare.new("#{fixtures}/a.png", "#{fixtures}/a.png")
+ compare = SnapDiff::Comparison.new("#{fixtures}/a.png", "#{fixtures}/a.png")
compare.processed
- CapybaraScreenshotDiff::ScreenshotAssertion.new(name).tap { |a| a.compare = compare }
+ SnapDiff::ScreenshotAssertion.new(name).tap { |a| a.compare = compare }
end
reporter.record(assertions + passing)
diff --git a/test/fixtures/rspec_after_hook_order_masking_spec.rb b/test/fixtures/rspec_after_hook_order_masking_spec.rb
index 674acd05..21e5f378 100644
--- a/test/fixtures/rspec_after_hook_order_masking_spec.rb
+++ b/test/fixtures/rspec_after_hook_order_masking_spec.rb
@@ -46,7 +46,6 @@
SnapDiff.config.root = Rails.root / "../test/fixtures/app"
SnapDiff.config.add_os_path = true
SnapDiff.config.add_driver_path = true
- SnapDiff.config.driver = ENV.fetch("SCREENSHOT_DRIVER", "chunky_png").to_sym
SnapDiff.config.tolerance = 0.5
# This fixture runs standalone in its own subprocess (no
# ActiveSupport::TestCase setup forcing this off), and CI sets $CI,
diff --git a/test/fixtures/rspec_pending_masking_spec.rb b/test/fixtures/rspec_pending_masking_spec.rb
index 0848639e..c44fd190 100644
--- a/test/fixtures/rspec_pending_masking_spec.rb
+++ b/test/fixtures/rspec_pending_masking_spec.rb
@@ -33,7 +33,6 @@
SnapDiff.config.root = Rails.root / "../test/fixtures/app"
SnapDiff.config.add_os_path = true
SnapDiff.config.add_driver_path = true
- SnapDiff.config.driver = ENV.fetch("SCREENSHOT_DRIVER", "chunky_png").to_sym
SnapDiff.config.tolerance = 0.5
# This fixture runs standalone in its own subprocess (no
# ActiveSupport::TestCase setup forcing this off), and CI sets $CI,
diff --git a/test/fixtures/rspec_spec.rb b/test/fixtures/rspec_spec.rb
index fbbc5347..14fa65f7 100644
--- a/test/fixtures/rspec_spec.rb
+++ b/test/fixtures/rspec_spec.rb
@@ -20,7 +20,6 @@
SnapDiff.config.root = Rails.root / "../test/fixtures/app"
SnapDiff.config.add_os_path = true
SnapDiff.config.add_driver_path = true
- SnapDiff.config.driver = ENV.fetch("SCREENSHOT_DRIVER", "chunky_png").to_sym
SnapDiff.config.tolerance = 0.5
end
@@ -36,7 +35,7 @@
it "use custom matcher" do
visit "/"
- expect(page).to match_screenshot("index", skip_stack_frames: 1, driver: :chunky_png)
+ expect(page).to match_screenshot("index", skip_stack_frames: 1)
end
it "does not conflicts with rspec methods" do
diff --git a/test/integration/browser_screenshot_test.rb b/test/integration/browser_screenshot_test.rb
index 5d83bca1..ae67b0ae 100644
--- a/test/integration/browser_screenshot_test.rb
+++ b/test/integration/browser_screenshot_test.rb
@@ -6,7 +6,9 @@ class BrowserScreenshotTest < SystemTestCase
setup do
SnapDiff.config.blur_active_element = true
@original_tolerance = SnapDiff.config.tolerance
- SnapDiff.config.tolerance = (SnapDiff.config.driver == :vips) ? 0.035 : 0.13
+ # Was branched on the driver (0.035 vips / 0.13 chunky_png); 2.1 left
+ # one backend, so only the vips figure survives.
+ SnapDiff.config.tolerance = 0.035
end
teardown do
diff --git a/test/integration/record_screenshot_test.rb b/test/integration/record_screenshot_test.rb
index 2bb2d747..03b78580 100644
--- a/test/integration/record_screenshot_test.rb
+++ b/test/integration/record_screenshot_test.rb
@@ -8,7 +8,9 @@ class RecordScreenshotTest < SystemTestCase
screenshot_group name[5..] unless SnapDiff.session.screenshot_namer.group
@original_tolerance = SnapDiff.config.tolerance
- SnapDiff.config.tolerance = (SnapDiff.config.driver == :vips) ? 0.035 : 0.7
+ # Was branched on the driver (0.035 vips / 0.7 chunky_png); 2.1 left
+ # one backend, so only the vips figure survives.
+ SnapDiff.config.tolerance = 0.035
end
teardown do
@@ -33,7 +35,7 @@ def test_record_index_as_webp
visit "/"
- screenshot "index-vips", screenshot_format: "webp", driver: :vips
+ screenshot "index-vips", screenshot_format: "webp"
end
def test_record_index_with_stability
diff --git a/test/legacy/errors_alias_test.rb b/test/legacy/errors_alias_test.rb
deleted file mode 100644
index 71f61dea..00000000
--- a/test/legacy/errors_alias_test.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-# The shared harness loads canonical entry points only, so a legacy-surface
-# test pulls in the v1 entry itself -- the require goes with the file in 2.1.
-require "capybara_screenshot_diff"
-
-# ADR-008 step 2: the error classes live in SnapDiff (snap_diff/errors);
-# the old CapybaraScreenshotDiff names are EAGER same-object aliases --
-# deliberately not const_missing shims -- so adopter rescue clauses and
-# defined?/const_defined? feature detection keep working unchanged.
-# Note the absence of any deprecation-silencing here: eager aliases never
-# warn, and the suite-wide guard in test_helper raises on unexpected
-# warnings, so these tests double as proof the aliases stay warning-free.
-#
-# LEGACY SURFACE (test/legacy/, see the Rakefile): deleted with lib/capybara*
-# in 2.1. The hierarchy assertions that outlive the aliases moved to
-# test/unit/errors_test.rb.
-class ErrorsAliasTest < ActiveSupport::TestCase
- # old constant path => new constant path
- MAPPING = {
- "CapybaraScreenshotDiff::CapybaraScreenshotDiffError" => "SnapDiff::Error",
- "CapybaraScreenshotDiff::ExpectationNotMet" => "SnapDiff::ExpectationNotMet",
- "CapybaraScreenshotDiff::UnstableImage" => "SnapDiff::UnstableImage",
- "CapybaraScreenshotDiff::WindowSizeMismatchError" => "SnapDiff::WindowSizeMismatchError"
- }.freeze
-
- MAPPING.each do |old_name, new_name|
- test "#{old_name} is the same object as #{new_name}" do
- assert_same Object.const_get(new_name), Object.const_get(old_name),
- "#{old_name} must alias the exact #{new_name} object"
- end
-
- # The regression the eager choice prevents: const_defined? (and
- # defined?) never trigger const_missing, so a lazy shim would make
- # feature detection by the old name silently return false.
- test "#{old_name} is visible to const_defined? without const_missing" do
- mod, leaf = old_name.rpartition("::").values_at(0, 2)
- assert Object.const_get(mod).const_defined?(leaf, false),
- "#{leaf} must be an eagerly-defined constant on #{mod}"
- assert defined?(CapybaraScreenshotDiff), "sanity: old namespace present"
- end
- end
-
- test "rescue by old name catches an error raised under the new name" do
- caught = nil
- begin
- raise SnapDiff::ExpectationNotMet.new("probe", caller)
- rescue CapybaraScreenshotDiff::ExpectationNotMet => e
- caught = e
- end
- assert_equal "probe", caught.message
- end
-end
diff --git a/test/legacy/legacy_config_accessors_test.rb b/test/legacy/legacy_config_accessors_test.rb
deleted file mode 100644
index c773c95f..00000000
--- a/test/legacy/legacy_config_accessors_test.rb
+++ /dev/null
@@ -1,196 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-# The shared harness loads canonical entry points only, so a legacy-surface
-# test pulls in the v1 entry itself -- the require goes with the file in 2.1.
-require "capybara_screenshot_diff"
-
-# LEGACY SURFACE (test/legacy/, see the Rakefile).
-#
-# The v1 half of snap_diff_config_test.rb: SnapDiff::LegacyShims generates
-# the old Capybara::Screenshot / Capybara::Screenshot::Diff mattr_accessors
-# as a second VIEW of the one SnapDiff::Config storage. Everything here is
-# about that view -- the mapping's completeness, and that a write through
-# either surface is visible from the other. Verbatim from the canonical
-# file, which keeps the Config-only half; both go on passing until 3.0
-# deletes legacy_shims.rb, this file, and the trees they serve.
-class LegacyConfigAccessorsTest < ActiveSupport::TestCase
- def config
- SnapDiff.config
- end
-
- # Reflection-based completeness check, reworked for the ADR-008 storage
- # inversion (the old version derived settings from mattr class variables,
- # which no longer exist). Two directions:
- #
- # (a) every singleton writer on the legacy modules is a mapped config
- # setting -- a future `mattr_accessor :foo` (active_support's ext is
- # one require away) or hand-rolled writer would create unmapped
- # storage invisible to SnapDiff.config;
- # (b) SnapDiff.config stores exactly one ivar per declared setting (that
- # half stays in the canonical file: Config outlives the mapping).
- NON_CONFIG_WRITERS = [].freeze # currently no non-config writer API on the legacy modules
-
- test "every legacy singleton writer is covered by SnapDiff::LegacyShims::CONFIG_MAPPING" do
- covered = SnapDiff::LegacyShims::CONFIG_MAPPING.values
-
- [Capybara::Screenshot, Capybara::Screenshot::Diff].each do |mod|
- writers = mod.singleton_class.public_instance_methods(false).grep(/=\z/) - NON_CONFIG_WRITERS
-
- assert_operator writers.size, :>, 0, "#{mod} lost all its config writers"
- writers.each do |writer|
- mattr = writer.to_s.delete_suffix("=").to_sym
-
- assert_includes covered, [mod, mattr],
- "#{mod}.#{writer} is a config writer with no SnapDiff::Config mapping. " \
- "Add an entry to LegacyShims::CONFIG_MAPPING (rename the key if `#{mattr}` collides " \
- "with an existing one, as `enabled` does), or add it to NON_CONFIG_WRITERS " \
- "if it is deliberately not a config setting."
- end
- end
- end
-
- # The two halves of the split are only safe while they agree: Config
- # declares the settings and knows nothing about the legacy holders,
- # LegacyShims says which holder each one is exposed on. A setting in one
- # and not the other is either storage with no v1 accessor or a v1
- # accessor delegating to a setting that does not exist.
- test "LegacyShims::CONFIG_MAPPING covers exactly Config::SETTINGS" do
- assert_equal SnapDiff::Config::SETTINGS, SnapDiff::LegacyShims::CONFIG_MAPPING.keys
- end
-
- test "every mapped setting is readable via config and equal to its mattr_accessor's value" do
- SnapDiff::LegacyShims::CONFIG_MAPPING.each do |name, (mod, mattr)|
- expected = mod.public_send(mattr)
- actual = config.public_send(name)
-
- if expected.nil?
- assert_nil actual, "config.#{name} should equal #{mod}.#{mattr}"
- else
- assert_equal expected, actual, "config.#{name} should equal #{mod}.#{mattr}"
- end
- end
- end
-
- test "writing fail_if_new via the old mattr_accessor is visible via config, and back" do
- original = Capybara::Screenshot::Diff.fail_if_new
-
- begin
- Capybara::Screenshot::Diff.fail_if_new = true
- assert_equal true, config.fail_if_new
-
- config.fail_if_new = false
- assert_equal false, Capybara::Screenshot::Diff.fail_if_new
- ensure
- Capybara::Screenshot::Diff.fail_if_new = original
- end
- end
-
- test "writing window_size via the old mattr_accessor is visible via config, and back" do
- original = Capybara::Screenshot.window_size
-
- begin
- Capybara::Screenshot.window_size = [1280, 1024]
- assert_equal [1280, 1024], config.window_size
-
- config.window_size = [800, 600]
- assert_equal [800, 600], Capybara::Screenshot.window_size
- ensure
- Capybara::Screenshot.window_size = original
- end
- end
-
- # Capybara::Screenshot.enabled and Capybara::Screenshot::Diff.enabled are
- # two independent settings that happen to share a bare name in their own
- # modules (see Capybara::Screenshot.active?, which reads both). Config is
- # flat, so it cannot expose two attributes both called `enabled` -- the
- # Screenshot-side one is renamed `screenshot_enabled`. This test proves
- # the rename didn't accidentally collapse them into one shared value.
- test "screenshot_enabled and enabled stay independent settings under Config" do
- original_screenshot = Capybara::Screenshot.enabled
- original_diff = Capybara::Screenshot::Diff.enabled
-
- begin
- config.screenshot_enabled = true
- config.enabled = false
-
- assert_equal true, Capybara::Screenshot.enabled
- assert_equal false, Capybara::Screenshot::Diff.enabled
- assert_equal true, config.screenshot_enabled
- assert_equal false, config.enabled
- ensure
- Capybara::Screenshot.enabled = original_screenshot
- Capybara::Screenshot::Diff.enabled = original_diff
- end
- end
-
- # ADR-008 step 7b moved this precedence rule from
- # Capybara::Screenshot.active? into Config#active?, and found it had no
- # test at all: replacing the whole expression with a bare `enabled` kept
- # all 529 unit tests green. The full truth table is pinned here, through
- # both the canonical method and the legacy forwarder, so it cannot move
- # again unnoticed. (The canonical file pins the Config#active? column on
- # its own, so the rule survives this file's deletion.)
- #
- # The rule: the Screenshot-side flag wins whenever it was set to anything
- # at all; only a nil there falls through to the Diff-side flag.
- ACTIVE_TRUTH_TABLE = [
- [true, true, true],
- [true, false, true],
- [false, true, false],
- [false, false, false],
- [nil, true, true],
- [nil, false, false]
- ].freeze
-
- test "active? gives Screenshot.enabled precedence and only falls through on nil" do
- original_screenshot = Capybara::Screenshot.enabled
- original_diff = Capybara::Screenshot::Diff.enabled
-
- ACTIVE_TRUTH_TABLE.each do |screenshot_enabled, enabled, expected|
- config.screenshot_enabled = screenshot_enabled
- config.enabled = enabled
- context = "screenshot_enabled=#{screenshot_enabled.inspect}, enabled=#{enabled.inspect}"
-
- assert_equal expected, !!config.active?, "Config#active? with #{context}"
- assert_equal expected, !!Capybara::Screenshot.active?, "Capybara::Screenshot.active? with #{context}"
- end
- ensure
- Capybara::Screenshot.enabled = original_screenshot
- Capybara::Screenshot::Diff.enabled = original_diff
- end
-
- test "writing root through config round-trips through the same Pathname coercion" do
- original = Capybara::Screenshot.root
-
- begin
- config.root = "/tmp"
-
- assert_equal Pathname("/tmp"), Capybara::Screenshot.root
- assert_equal Pathname("/tmp"), config.root
- ensure
- Capybara::Screenshot.root = original
- end
- end
-
- test "SnapDiff.configure lets callers set values through the yielded config" do
- original = Capybara::Screenshot::Diff.tolerance
-
- begin
- SnapDiff.configure { |c| c.tolerance = 0.0321 }
- assert_equal 0.0321, Capybara::Screenshot::Diff.tolerance
- ensure
- Capybara::Screenshot::Diff.tolerance = original
- end
- end
-
- test "SnapDiff.start (v1-style two-arg yield) and SnapDiff.configure (single Config yield) coexist" do
- diff_yielded = []
- SnapDiff.start { |screenshot, diff| diff_yielded << [screenshot, diff] }
- assert_equal [[Capybara::Screenshot, Capybara::Screenshot::Diff]], diff_yielded
-
- config_yielded = []
- SnapDiff.configure { |c| config_yielded << c }
- assert_equal [config], config_yielded
- end
-end
diff --git a/test/legacy/legacy_config_default_timing_test.rb b/test/legacy/legacy_config_default_timing_test.rb
deleted file mode 100644
index 72592857..00000000
--- a/test/legacy/legacy_config_default_timing_test.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "open3"
-require "unit/config_default_timing_test" # single source of truth for the probe scripts
-
-# LEGACY SURFACE (test/legacy/, see the Rakefile).
-#
-# The v1 half of config_default_timing_test.rb. Two claims, both about the
-# old entry points and the old accessor view, both deleted in 2.1:
-#
-# 1. every legacy entry point produces the SAME require-time defaults and
-# the same freezing/liveness behaviour as the canonical ones -- proved by
-# re-running the canonical file's probe scripts verbatim under a v1
-# PROBE_ENTRY, so the two files can never drift;
-# 2. every mapped setting reads identically through SnapDiff.config and
-# through its legacy mattr_accessor. Together with (1) that is exactly
-# what the old `check_both` asserted: the value is right via config, and
-# the two surfaces cannot fork.
-class LegacyConfigDefaultTimingTest < ActiveSupport::TestCase
- ENTRY_POINTS = %w[
- capybara_screenshot_diff
- capybara_screenshot_diff/minitest
- capybara/screenshot/diff
- ].freeze
-
- def run_probe(script, env)
- out, status = Open3.capture2e(env, RbConfig.ruby, "-Ilib", "-e", script)
-
- assert status.success?, "probe failed:\n#{out}"
- end
-
- BOTH_SURFACES_SCRIPT = ConfigDefaultTimingTest::CHECK_HELPER + <<~'RUBY'
- require ENV.fetch("PROBE_ENTRY")
-
- SnapDiff::LegacyShims::CONFIG_MAPPING.each do |name, (mod, mattr)|
- check("SnapDiff.config.#{name} vs #{mod}.#{mattr}", mod.public_send(mattr), SnapDiff.config.public_send(name))
- end
- RUBY
-
- ENTRY_POINTS.each do |entry|
- test "#{entry}: defaults snapshot matches; ENV/pwd frozen at require, wait live" do
- run_probe(ConfigDefaultTimingTest::SNAPSHOT_SCRIPT, {"PROBE_ENTRY" => entry, "CI" => nil})
- end
-
- test "#{entry}: CI=1 before require turns fail_if_new on; unset after require does not turn it off" do
- run_probe(ConfigDefaultTimingTest::CI_SET_SCRIPT, {"PROBE_ENTRY" => entry, "CI" => "1"})
- end
-
- test "#{entry}: Rails.root defined before require wins; reassigning it after require is not seen" do
- run_probe(ConfigDefaultTimingTest::RAILS_ROOT_SCRIPT, {"PROBE_ENTRY" => entry, "CI" => nil})
- end
-
- test "#{entry}: every mapped setting reads the same through config and its mattr_accessor" do
- run_probe(BOTH_SURFACES_SCRIPT, {"PROBE_ENTRY" => entry, "CI" => nil})
- end
- end
-end
diff --git a/test/legacy/legacy_entry_point_probe_test.rb b/test/legacy/legacy_entry_point_probe_test.rb
deleted file mode 100644
index f9dc189d..00000000
--- a/test/legacy/legacy_entry_point_probe_test.rb
+++ /dev/null
@@ -1,286 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "unit/support_load_probe_test" # single source of truth for the subprocess probe
-
-# LEGACY SURFACE (test/legacy/, see the Rakefile).
-#
-# The v1 half of support_load_probe_test.rb: what the OLD entry points must
-# still provide. Names and constants restored verbatim -- every assertion
-# here is about a name 3.0 deletes, so repointing them at SnapDiff would
-# have quietly turned this file into a duplicate of the canonical one.
-class LegacyEntryPointProbeTest < ActiveSupport::TestCase
- # Alias-completeness probe (the f89cea2 bug class): each documented entry
- # point must define its advertised constants when it is the ONLY require —
- # the acyclic redesign once narrowed capybara_screenshot_diff/minitest so
- # consumers lost CapybaraScreenshotDiff::DSL, and only one CI matrix leg
- # noticed. capybara_screenshot_diff/cucumber is not probed: it calls
- # World(...) at load, which only exists inside cucumber's runtime context.
- # Documented user-facing constants that must stay EAGER (see
- # snap_diff/legacy_shims.rb's exception list): const_defined? never
- # triggers const_missing, so a lazy shim makes `defined?` feature
- # detection in adopter code silently return nil.
- EAGER_USER_FACING = %w[
- Capybara::Screenshot::Diff::Reporters::Default
- Capybara::Screenshot::Diff::Comparison
- ].freeze
-
- # The subset of EAGER_USER_FACING that must resolve under EVERY entry
- # point, canonical ones included -- these are read directly (a version
- # string, a struct), not just feature-detected, so a canonical-only app
- # still hits them. The test above only covers the four legacy entries,
- # which is how VERSION silently disappeared from six entry points when the
- # core stopped requiring capybara/screenshot/diff/version.rb: nothing
- # loaded the forwarder that assigned it, and const_missing does not fire
- # for a constant legacy_shims deliberately leaves out of its map.
- EAGER_EVERYWHERE = %w[
- Capybara::Screenshot::Diff::VERSION
- Capybara::Screenshot::Diff::Comparison
- ].freeze
-
- ENTRY_POINTS = {
- "capybara_screenshot_diff" => %w[
- CapybaraScreenshotDiff::DSL Capybara::Screenshot::Os Capybara::Screenshot::Diff
- ] + EAGER_USER_FACING,
- "capybara_screenshot_diff/minitest" => %w[
- CapybaraScreenshotDiff::DSL CapybaraScreenshotDiff::Minitest::Assertions
- Capybara::Screenshot::Os Capybara::Screenshot::Diff
- ] + EAGER_USER_FACING,
- "capybara_screenshot_diff/rspec" => %w[
- CapybaraScreenshotDiff::DSL Capybara::Screenshot::Os Capybara::Screenshot::Diff
- ] + EAGER_USER_FACING,
- "capybara-screenshot-diff" => %w[
- CapybaraScreenshotDiff::DSL CapybaraScreenshotDiff::Minitest::Assertions
- Capybara::Screenshot::Os Capybara::Screenshot::Diff
- ] + EAGER_USER_FACING
- }.freeze
-
- test "every documented entry point defines its advertised constants standalone" do
- failures = ENTRY_POINTS.filter_map do |entry, constants|
- probe(entry, <<~RUBY)
- require #{entry.inspect}
- missing = #{constants.inspect}.reject { |c| Object.const_defined?(c) }
- abort("missing: \#{missing.join(", ")}") unless missing.empty?
- RUBY
- end
-
- assert_empty failures, <<~MSG
- Entry point(s) no longer provide their advertised constants standalone:
-
- #{failures.join("\n")}
- MSG
- end
-
- # The legacy entries had the mirror image of the beta3 canonical hole:
- # some of them stopped loading the umbrella, so CapybaraScreenshotDiff.verify
- # and friends vanished while `defined?(CapybaraScreenshotDiff)` still passed.
- LEGACY_SESSION_SURFACE = %w[
- verify reset reporters finalize_reporters! assertions registry
- pending_screenshots_message
- ].freeze
-
- LEGACY_ENTRY_POINTS = %w[
- capybara-screenshot-diff
- snap_diff-capybara
- capybara_screenshot_diff
- capybara_screenshot_diff/minitest
- capybara_screenshot_diff/rspec
- capybara_screenshot_diff/cucumber
- capybara_screenshot_diff/static
- capybara/screenshot/diff
- capybara/screenshot/diff/cucumber
- ].freeze
-
- test "every legacy entry point keeps the CapybaraScreenshotDiff session surface" do
- failures = LEGACY_ENTRY_POINTS.filter_map do |entry|
- probe(entry, <<~RUBY)
- require #{entry.inspect}
- missing = #{LEGACY_SESSION_SURFACE.inspect}.reject { |m| CapybaraScreenshotDiff.respond_to?(m) }
- abort("missing: \#{missing.join(", ")}") unless missing.empty?
- RUBY
- end
-
- assert_empty failures, <<~MSG
- Legacy entry point(s) leave CapybaraScreenshotDiff half-present
- (the module answers `defined?` but not its own session methods):
-
- #{failures.join("\n")}
- MSG
- end
-
- # SnapDiff.start moved here out of the canonical CANONICAL_SURFACE gate: it
- # is defined in legacy_shims.rb and yields the two v1 config holders, so a
- # canonical gate demanding it fails the moment 3.0 deletes them. It is
- # still a documented v1 method, so the per-entry-point availability claim
- # the canonical gate used to make lives on here -- for the entries that
- # actually keep it. (What it yields is pinned in legacy_forwarders_test.)
- test "SnapDiff.start is available from every legacy entry point" do
- failures = LEGACY_ENTRY_POINTS.filter_map do |entry|
- probe(entry, <<~RUBY)
- require #{entry.inspect}
- abort("SnapDiff.start missing") unless SnapDiff.respond_to?(:start)
- RUBY
- end
-
- assert_empty failures, failures.join("\n")
- end
-
- # Every documented entry point, canonical and legacy. capybara_screenshot_diff/dsl
- # is listed only here: it is not in ENTRY_POINTS or LEGACY_ENTRY_POINTS, which
- # is exactly why it was the legacy entry that lost VERSION unnoticed.
- ALL_ENTRY_POINTS = (
- SupportLoadProbeTest::CANONICAL_ENTRY_POINTS.keys + LEGACY_ENTRY_POINTS + %w[capybara_screenshot_diff/dsl]
- ).uniq.freeze
-
- test "the eager user-facing constants resolve under every entry point" do
- failures = ALL_ENTRY_POINTS.filter_map do |entry|
- probe(entry, <<~RUBY)
- require #{entry.inspect}
- missing = #{EAGER_EVERYWHERE.inspect}.reject { |c| Object.const_defined?(c) }
- abort("missing: \#{missing.join(", ")}") unless missing.empty?
- RUBY
- end
-
- assert_empty failures, <<~MSG
- Entry point(s) no longer resolve constants that are supposed to be eager
- everywhere. `defined?` returns nil for these and const_missing does not
- fire, so adopter feature detection fails silently:
-
- #{failures.join("\n")}
- MSG
- end
-
- # The canonical half of this claim lives in support_load_probe_test.rb; the
- # v1 entry points get the same treatment here because the cycle they used
- # to load through (drivers.rb <-> utils.rb) shouted at every user whose
- # suite runs with warnings on -- which Rake::TestTask does by default.
- test "no legacy entry point emits a circular require warning under -w" do
- failures = LEGACY_ENTRY_POINTS.filter_map do |entry|
- noise = SupportLoadProbeTest.verbose_load(entry).lines.grep(/circular require/)
- "require \"#{entry}\" ->\n#{noise.join}" unless noise.empty?
- end
-
- assert_empty failures, <<~MSG
- Legacy entry point(s) load through a `require` cycle:
-
- #{failures.join("\n")}
- MSG
- end
-
- # Every legacy constant a consumer might touch, as of v1.12.0's surface.
- # UPGRADING.md tells adopters to migrate their `require` line FIRST and
- # rename constants afterwards, so this half-migrated state -- canonical
- # require, v1 constants -- is a supported one, not an exotic edge case.
- # It used to kill a whole suite at load on `Capybara::Screenshot::Os`.
- LEGACY_CONSTANTS = %w[
- Capybara::Screenshot::Os
- Capybara::Screenshot::BrowserHelpers
- Capybara::Screenshot::Screenshoter
- Capybara::Screenshot::Diff::Vcs
- Capybara::Screenshot::Diff::StableScreenshoter
- Capybara::Screenshot::Diff::ImagePreprocessor
- Capybara::Screenshot::Diff::AreaCalculator
- Capybara::Screenshot::Diff::AnnotationService
- Capybara::Screenshot::Diff::Utils
- Capybara::Screenshot::Diff::ScreenshotMatcher
- Capybara::Screenshot::Diff::Drivers
- Capybara::Screenshot::Diff::Drivers::BaseDriver
- Capybara::Screenshot::Diff::Drivers::ChunkyPNGDriver
- Capybara::Screenshot::Diff::ImageCompare
- Capybara::Screenshot::Diff::Difference
- Capybara::Screenshot::Diff::Comparison
- Capybara::Screenshot::Diff::VERSION
- Capybara::Screenshot::Diff::LOADED_DRIVERS
- Capybara::Screenshot::Diff::AVAILABLE_DRIVERS
- Capybara::Screenshot::Diff::Reporters::Default
- Region
- CapybaraScreenshotDiff::RED_RGBA
- CapybaraScreenshotDiff::ORANGE_RGBA
- CapybaraScreenshotDiff::SnapManager
- CapybaraScreenshotDiff::Snap
- CapybaraScreenshotDiff::ScreenshotNamer
- CapybaraScreenshotDiff::AttemptsReporter
- CapybaraScreenshotDiff::BacktraceFilter
- CapybaraScreenshotDiff::ErrorWithFilteredBacktrace
- CapybaraScreenshotDiff::ScreenshotAssertion
- CapybaraScreenshotDiff::AssertionRegistry
- CapybaraScreenshotDiff::CapybaraScreenshotDiffError
- CapybaraScreenshotDiff::ExpectationNotMet
- CapybaraScreenshotDiff::UnstableImage
- CapybaraScreenshotDiff::WindowSizeMismatchError
- CapybaraScreenshotDiff::DSL
- CapybaraScreenshotDiff::Minitest::Assertions
- CapybaraScreenshotDiff::Reporters::HTML
- ].freeze
-
- test "every legacy constant resolves under a canonical-only require" do
- failures = ALL_ENTRY_POINTS.filter_map do |entry|
- probe(entry, <<~RUBY)
- ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = "1"
- require #{entry.inspect}
- # Resolvability only -- same-object identity is pinned separately by
- # test/legacy/namespace_forwarding_test.rb.
- broken = #{LEGACY_CONSTANTS.inspect}.filter_map do |name|
- begin
- Object.const_get(name)
- nil
- rescue NameError => e
- "\#{name}: \#{e.message.lines.first.strip}"
- end
- end
- abort(broken.join("\n")) unless broken.empty?
- RUBY
- end
-
- assert_empty failures, <<~MSG
- Legacy constant(s) do not resolve under these entry points. A half-
- migrated app -- canonical require, v1 constants, exactly what
- UPGRADING.md walks users into -- dies on the first reference:
-
- #{failures.join("\n")}
- MSG
- end
-
- # Vips is optional, so its driver leaf only has to resolve where the
- # library is actually installed.
- test "the vips driver leaf resolves under a canonical-only require" do
- skip "vips not available in this environment" unless SnapDiff::Drivers.available.include?(:vips)
-
- assert_nil SupportLoadProbeTest.probe("snap_diff", <<~RUBY)
- ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = "1"
- require "snap_diff"
- Object.const_get("Capybara::Screenshot::Diff::Drivers::VipsDriver")
- RUBY
- end
-
- # A legacy name whose replacement genuinely cannot load must say so in the
- # user's vocabulary -- UPGRADING.md -- not leak a bare "uninitialized
- # constant SnapDiff::Something" from gem internals.
- test "an unloadable legacy constant points at the upgrade guide" do
- out, status = Open3.capture2e(
- RbConfig.ruby, "-Ilib", "-e", <<~RUBY, chdir: File.expand_path("../..", __dir__)
- ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = "1"
- require "snap_diff"
- mod = Module.new
- SnapDiff::LegacyShims.install(mod, "Old::Prefix", {Gone: "SnapDiff::NoSuchThing"})
- begin
- mod::Gone
- rescue NameError => e
- puts e.message
- end
- RUBY
- )
-
- assert status.success?, out
- assert_includes out, "SnapDiff::NoSuchThing"
- assert_includes out, "docs/UPGRADING.md"
- refute_includes out, "Reference `SnapDiff::NoSuchThing` directly",
- "must not advise referencing a name we just failed to load"
- end
-
- private
-
- def probe(entry, script)
- SupportLoadProbeTest.probe(entry, script)
- end
-end
diff --git a/test/legacy/legacy_forwarders_test.rb b/test/legacy/legacy_forwarders_test.rb
deleted file mode 100644
index 996ba109..00000000
--- a/test/legacy/legacy_forwarders_test.rb
+++ /dev/null
@@ -1,124 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "open3"
-# The shared harness loads canonical entry points only, so a legacy-surface
-# test pulls in the v1 entry itself -- the require goes with the file in 2.1.
-require "capybara_screenshot_diff"
-require "capybara_screenshot_diff/static"
-
-# LEGACY SURFACE (test/legacy/, see the Rakefile).
-#
-# The identity claims that make the old CapybaraScreenshotDiff module a
-# *view* of the canonical state rather than a second copy of it, plus the
-# v1-shaped SnapDiff.start. Collected here from the canonical tests they
-# used to sit in (registry_concurrency_test, reporters_mutex_test,
-# snap_diff_test): a mechanical repoint would have turned each of them into
-# `assert_same X, X`, which is how a real claim quietly becomes a tautology.
-# Verbatim, so the v1 contract keeps exactly the coverage it had.
-class LegacyForwardersTest < ActiveSupport::TestCase
- setup do
- # These resolve old-namespace names on purpose; the suite-wide guard in
- # test_helper raises on unexpected shim warnings.
- @original_silence = SnapDiff.silence_deprecations
- SnapDiff.silence_deprecations = true
- end
-
- teardown do
- SnapDiff.silence_deprecations = @original_silence
- end
-
- # ADR-008 step 6: SnapDiff.session is the canonical accessor and
- # CapybaraScreenshotDiff.registry a forwarder over it -- they must hand
- # back the *same* object, not two registries that happen to look alike.
- test "SnapDiff.session and CapybaraScreenshotDiff.registry are the same object" do
- assert_same SnapDiff.session, CapybaraScreenshotDiff.registry
-
- SnapDiff.session.record_new_screenshot("shared_object_probe")
- assert_equal ["shared_object_probe"], CapybaraScreenshotDiff.new_screenshots
- ensure
- SnapDiff.session.reset
- end
-
- # ADR-008 step 6: SnapDiff::Reporting.register is the canonical way in;
- # CapybaraScreenshotDiff.reporters stays as the compat view of the same
- # array, so a registration must be visible through both.
- test "register appends to the array CapybaraScreenshotDiff.reporters exposes" do
- original_reporters = CapybaraScreenshotDiff.reporters.dup
- CapybaraScreenshotDiff.reporters.clear
- reporter = Object.new
-
- assert_same reporter, SnapDiff::Reporting.register(reporter)
- assert_same SnapDiff::Reporting.reporters, CapybaraScreenshotDiff.reporters
- assert_includes CapybaraScreenshotDiff.reporters, reporter
- ensure
- CapybaraScreenshotDiff.reporters.clear
- CapybaraScreenshotDiff.reporters.concat(original_reporters)
- end
-
- test "CapybaraScreenshotDiff.reporters_mutex is the canonical Reporting mutex" do
- assert_same SnapDiff::Reporting.mutex, CapybaraScreenshotDiff.reporters_mutex
- end
-
- test "Capybara::Screenshot::Diff::ImageCompare aliases SnapDiff::Comparison" do
- assert_same SnapDiff::Comparison, Capybara::Screenshot::Diff::ImageCompare
- end
-
- test "CapybaraScreenshotDiff.serve forwards to SnapDiff.serve, custom root included" do
- original_root = SnapDiff.config.root
-
- CapybaraScreenshotDiff.serve("test/fixtures", root: "/tmp")
-
- assert_equal Pathname("/tmp"), SnapDiff.config.root
- ensure
- Capybara.app = Rails.application
- SnapDiff.config.root = original_root
- end
-
- # Acyclicity contract (the #208 deadlock-class fix): the lean
- # `require "snap_diff"` entry must NEVER pull the umbrella
- # capybara_screenshot_diff.rb back in. The old autoload wiring had
- # snap_diff <-> capybara_screenshot_diff requiring each other, which
- # produced load-order deadlocks/partially-initialized constants; #208
- # broke the cycle, but until now only discipline guarded it -- a probe
- # that reintroduced the cycle left the whole suite green. This asserts
- # the contract as data: after a bare require, the umbrella file must be
- # absent from $LOADED_FEATURES.
- #
- # Lives here rather than in snap_diff_test: its subject is the v1
- # umbrella, and once 3.0 deletes that file the grep below is empty by
- # construction and the guard can never fail again.
- test "bare require \"snap_diff\" never loads the umbrella capybara_screenshot_diff" do
- script = <<~RUBY
- require "snap_diff"
- umbrella = $LOADED_FEATURES.grep(%r{/lib/capybara_screenshot_diff\\.rb\\z})
- abort("umbrella loaded via: \#{umbrella.join(", ")}") unless umbrella.empty?
- RUBY
-
- out, status = Open3.capture2e(RbConfig.ruby, "-Ilib", "-e", script)
-
- assert status.success?, "expected bare `require \"snap_diff\"` to keep the umbrella unloaded, got:\n#{out}"
- end
-
- test ".start yields the same objects Diff.configure yields" do
- yielded = []
- Capybara::Screenshot::Diff.configure { |screenshot, diff| yielded << [screenshot, diff] }
-
- started = []
- SnapDiff.start { |screenshot, diff| started << [screenshot, diff] }
-
- assert_equal yielded, started
- end
-
- test ".start applies a setting like Diff.configure does" do
- original = SnapDiff.config.tolerance
-
- begin
- SnapDiff.start { |_screenshot, diff| diff.tolerance = 0.0123 }
-
- assert_equal 0.0123, SnapDiff.config.tolerance
- ensure
- SnapDiff.config.tolerance = original
- end
- end
-end
diff --git a/test/legacy/legacy_namespace_deprecation_test.rb b/test/legacy/legacy_namespace_deprecation_test.rb
deleted file mode 100644
index 5063fbdd..00000000
--- a/test/legacy/legacy_namespace_deprecation_test.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "open3"
-require "snap_diff/deprecation"
-require "legacy/namespace_forwarding_test" # single source of truth for the old->new MAPPING
-
-# ADR-004 v2 step 6: resolving an old-namespace constant emits a deprecation
-# warning -- exactly once per constant per process, naming the SnapDiff
-# replacement, silenceable through the pre-existing SnapDiff::Deprecation
-# switches (SnapDiff.silence_deprecations / SNAP_DIFF_SILENCE_DEPRECATIONS).
-# Same-object identity for every pair stays pinned by
-# namespace_forwarding_test.rb; this file pins only the warning behavior.
-#
-# LEGACY SURFACE (test/legacy/, see the Rakefile): deleted with lib/capybara*
-# and snap_diff/deprecation.rb in 2.1.
-class LegacyNamespaceDeprecationTest < ActiveSupport::TestCase
- # Documented exceptions that stay EAGER (real constants, never warn):
- # - Os / DSL: advertised entry-point constants, probed with
- # Object.const_defined? by support_load_probe_test.rb -- const_defined?
- # never triggers const_missing, so a lazy shim would break that contract.
- # - VERSION: the gemspec resolves Capybara::Screenshot::Diff::VERSION at
- # build time; a lazy shim would make every `gem build` warn.
- EAGER_SILENT = %w[
- Capybara::Screenshot::Os
- CapybaraScreenshotDiff::DSL
- Capybara::Screenshot::Diff::VERSION
- ].freeze
-
- # Real constants on the shared SnapDiff::Drivers module (the Drivers alias
- # is same-object by contract), so const_missing can never fire for the leaf
- # name; resolving them through the old path still warns for ...::Drivers.
- WARNED_VIA_PARENT = %w[
- Capybara::Screenshot::Diff::Drivers::ChunkyPNGDriver
- Capybara::Screenshot::Diff::Drivers::VipsDriver
- ].freeze
-
- def setup
- @original_silence = SnapDiff.silence_deprecations
- # silence_deprecations? also reads the env var live, so an inherited
- # SNAP_DIFF_SILENCE_DEPRECATIONS would defeat the accessor below.
- @original_silence_env = ENV.delete("SNAP_DIFF_SILENCE_DEPRECATIONS")
- SnapDiff.silence_deprecations = false
- SnapDiff::Deprecation.reset!
- # These tests emit (and capture) the warnings on purpose; keep the
- # suite-wide raise-on-deprecation guard from test_helper out of the way.
- SnapDiffDeprecationGuard.expected = true
- end
-
- def teardown
- SnapDiffDeprecationGuard.expected = false
- SnapDiff.silence_deprecations = @original_silence
- ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = @original_silence_env if @original_silence_env
- SnapDiff::Deprecation.reset!
- end
-
- def capture_warnings
- _out, err = capture_io { yield }
- err.lines.reject(&:empty?)
- end
-
- NamespaceForwardingTest::MAPPING.each do |old_name, new_name|
- next if EAGER_SILENT.include?(old_name) || WARNED_VIA_PARENT.include?(old_name)
-
- define_method(:"test_#{old_name}_warns_once_pointing_at_#{new_name}") do
- lines = capture_warnings do
- 2.times { Object.const_get(old_name) }
- end
-
- own_lines = lines.grep(/`#{Regexp.escape(old_name)}` is deprecated/)
- assert_equal 1, own_lines.size,
- "expected exactly one deprecation warning for #{old_name} across two resolutions, " \
- "got #{own_lines.size} in:\n#{lines.join}"
- assert_match(/#{Regexp.escape(new_name)}/, own_lines.first,
- "warning for #{old_name} should name the replacement #{new_name}")
- end
- end
-
- test "eager compatibility constants stay defined and silent" do
- EAGER_SILENT.each do |name|
- assert Object.const_defined?(name), "#{name} must stay an eagerly-defined constant"
- end
-
- lines = capture_warnings { EAGER_SILENT.each { |name| Object.const_get(name) } }
-
- assert_empty lines, "eager compatibility constants must not warn"
- end
-
- test "silencing suppresses shim warnings" do
- SnapDiff.silence_deprecations = true
-
- lines = capture_warnings { Object.const_get("Capybara::Screenshot::Diff::ImageCompare") }
-
- assert_empty lines
- end
-
- test "unmapped old-namespace constants still raise NameError" do
- error = assert_raises(NameError) { Capybara::Screenshot::Diff.const_get(:DoesNotExist) }
- assert_match(/DoesNotExist/, error.message)
-
- assert_raises(NameError) { CapybaraScreenshotDiff.const_get(:DoesNotExist) }
- assert_raises(NameError) { Capybara::Screenshot.const_get(:DoesNotExist) }
- end
-
- test "old constants resolve to the same objects while warning" do
- _out, _err = capture_io do
- assert_same SnapDiff::Comparison, Object.const_get("Capybara::Screenshot::Diff::ImageCompare")
- assert_same SnapDiff::SnapManager, Object.const_get("CapybaraScreenshotDiff::SnapManager")
- end
- end
-
- # --- subprocess probes: the gem's OWN code must never warn -------------
-
- PROJECT_ROOT = File.expand_path("../..", __dir__)
- FIXTURE_IMAGE = File.expand_path("../fixtures/images/a.png", __dir__)
-
- # Legacy entry points exercise load + a real comparison + the session
- # lifecycle; the canonical snap_diff entry exercises load + comparison +
- # config. Zero deprecation output allowed: internal code must reference
- # SnapDiff names only.
- ENTRY_POINT_PROBES = {
- "capybara_screenshot_diff" => :legacy,
- "capybara_screenshot_diff/minitest" => :legacy,
- "capybara_screenshot_diff/rspec" => :legacy,
- "capybara-screenshot-diff" => :legacy,
- "snap_diff" => :canonical
- }.freeze
-
- test "loading each entry point and running a trivial comparison emits no deprecation output" do
- failures = ENTRY_POINT_PROBES.filter_map do |entry, kind|
- body = "require #{entry.inspect}\n"
- # The probe itself names chunky_png (the one driver present on every
- # box, so the comparison below is deterministic), and naming it is a
- # user choice that warns about the 2.1 removal by design. Silence THAT
- # channel only -- the legacy-namespace warnings this test is actually
- # about stay live.
- body << "SnapDiff::Removal.suppress!\n"
- body << "raise \"compare failed\" unless SnapDiff.compare(#{FIXTURE_IMAGE.inspect}, #{FIXTURE_IMAGE.inspect}, driver: :chunky_png).quick_equal?\n"
- if kind == :legacy
- body << "CapybaraScreenshotDiff.assertions_present?\n"
- body << "CapybaraScreenshotDiff.pending_screenshots_message\n"
- body << "CapybaraScreenshotDiff.reset\n"
- else
- body << "SnapDiff.config\n"
- end
-
- _out, err, status = Open3.capture3(RbConfig.ruby, "-Ilib", "-e", body, chdir: PROJECT_ROOT)
-
- if !status.success?
- "require \"#{entry}\": probe failed:\n#{err}"
- elsif err.include?("deprecation")
- "require \"#{entry}\": internal use emitted deprecation output:\n#{err}"
- end
- end
-
- assert_empty failures, failures.join("\n\n")
- end
-
- test "touching an old constant in a fresh unsilenced process does warn (control probe)" do
- script = 'require "capybara_screenshot_diff"; Capybara::Screenshot::Diff::ImageCompare'
- _out, err, status = Open3.capture3(RbConfig.ruby, "-Ilib", "-e", script, chdir: PROJECT_ROOT)
-
- assert status.success?, err
- assert_match(/\[snap_diff deprecation\].*Capybara::Screenshot::Diff::ImageCompare.*SnapDiff::Comparison/, err)
- end
-end
diff --git a/test/legacy/legacy_tree_is_alias_only_test.rb b/test/legacy/legacy_tree_is_alias_only_test.rb
deleted file mode 100644
index d2dfe43c..00000000
--- a/test/legacy/legacy_tree_is_alias_only_test.rb
+++ /dev/null
@@ -1,162 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-
-# ADR-008 step 7: the mechanical gate that keeps 3.0 a `git rm`.
-#
-# lib/capybara/ and lib/capybara_screenshot_diff/ are the v1 compatibility
-# surface. Every unit of behaviour has moved to lib/snap_diff/, so what is
-# left must be nothing but requires, namespace reopening, constant aliases
-# and one-line forwarders. If that stays true, dropping v1 support in 2.1 is
-# a deletion; the moment real logic lands back in these trees it becomes a
-# refactor. This test fails the second that happens, naming the file.
-#
-# LEGACY SURFACE (test/legacy/, see the Rakefile): deleted with the trees it
-# scans in 2.1. Its mirror image, core_tree_has_no_legacy_deps_test.rb,
-# guards what 3.0 KEEPS and so stays in test/unit/.
-class LegacyTreeIsAliasOnlyTest < ActiveSupport::TestCase
- LIB = Pathname.new(__dir__).join("../../lib").expand_path
-
- LEGACY_FILES = (
- Dir[LIB.join("capybara*.rb")] +
- Dir[LIB.join("capybara/**/*.rb")] +
- Dir[LIB.join("capybara_screenshot_diff/**/*.rb")]
- ).map { |path| Pathname.new(path) }.sort.freeze
-
- # THE ALLOWLIST -- and as of ADR-008 step 7b it is EMPTY: not one file in
- # the v1 trees holds logic any more. config_legacy.rb was the last entry;
- # step 7b moved its derived config (.active? precedence, .screenshot_area
- # path assembly, .default_options incl. the vips tolerance literal) into
- # SnapDiff::Config, and the 3.0-readiness pass moved the remaining
- # forwarders and the legacy accessor generator into
- # snap_diff/legacy_shims.rb -- the one file that holds the v1 surface as
- # code, and that 3.0 deletes together with these trees. There is not a
- # single `def` left here.
- #
- # Keep it empty. Adding an entry back is a decision to keep behaviour on
- # the v1 side of the 3.0 deletion, so it needs a written reason here AND
- # an ADR-008 update -- never just to turn a red build green.
- ALLOWED_WITH_CODE = {}.freeze
-
- # A `def` in these trees is only acceptable as a THREE-line forwarder --
- # signature, ONE delegating expression, `end` -- and this is that
- # expression: a single method-call chain rooted at SnapDiff, passing its
- # arguments straight through.
- #
- # SIMPLE_ARGS is where the strictness lives. Names, commas, `*`/`**`/`&`
- # and keyword colons -- and nothing else. No parentheses, so a nested call
- # cannot appear; no `.`, so neither can a bare receiver call; no `?`, `"`
- # or `=`, so no conditional, literal or assignment. Two escapes this
- # closes, both of which ran arbitrary code past the previous rule:
- #
- # SnapDiff.config.x(File.exist?("/etc/passwd") ? raise("boom") : ENV.fetch("HOME"))
- # SnapDiff.config.tap { |c| File.write("/tmp/pwned", c.inspect); exit 1 }
- #
- # The second also slipped past the semicolon check, because the walk below
- # steps over a def's body line -- fixed there.
- #
- # No block form at all: nothing in these trees has a `def` left, and an
- # unbounded `{ ... }` is exactly the hole above. A yield-through forwarder
- # that genuinely needs one is a decision to re-open here, deliberately.
- # `...` is Ruby's argument forwarding -- the purest forwarder there is
- # (CapybaraScreenshotDiff.serve uses it), so it is spelled out rather than
- # let in by loosening the character set.
- SIMPLE_ARGS = /\.\.\.|[\w\s,:*&]*/
- FORWARDER_BODY = /\A
- SnapDiff(::[A-Z]\w*)* # SnapDiff, SnapDiff::Reporting, ...
- (\.[a-z_]\w*[?!]?)+ # .config.active?, .compare, ...
- (\((?:#{SIMPLE_ARGS})\))? # at most one argument list, pass-through only
- \z/x
-
- # Shapes that are pure compatibility plumbing rather than behaviour.
- ALIAS_SHAPES = /\A(
- require(_relative)?\s |
- autoload\s |
- (module|class)\s |
- end\z |
- private\z |
- (extend|include)\s |
- [A-Z]\w*\s*=\s | # constant alias: Foo = SnapDiff::Foo
- def_delegators?\s
- )/x
-
- test "every legacy file is aliases and forwarders only" do
- refute_empty LEGACY_FILES, "legacy tree glob matched nothing -- the gate would pass vacuously"
-
- offenders = LEGACY_FILES.reject { |file| allowed?(file) }.flat_map { |file| offences(file) }
-
- assert_empty offenders, <<~MSG
- Real logic found in the v1 compatibility trees. Move it to lib/snap_diff/
- (or, if it genuinely must stay, add the file to ALLOWED_WITH_CODE with a
- written reason and update ADR-008):
-
- #{offenders.join("\n")}
- MSG
- end
-
- # The pinned method inventory that used to narrow config_legacy.rb's
- # allowlist entry is gone with the entry itself (ADR-008 step 7b): with
- # nothing allowlisted, the general rule above already checks every `def`
- # in the tree, config_legacy.rb's included.
- test "the allowlist is empty, so nothing is exempt from the general rule" do
- assert_empty ALLOWED_WITH_CODE,
- "an exemption came back -- see the comment on ALLOWED_WITH_CODE before keeping it"
- end
-
- private
-
- def allowed?(file)
- ALLOWED_WITH_CODE.key?(file.relative_path_from(LIB).to_s)
- end
-
- # Walks the file's significant lines. A `def` is only acceptable when its
- # whole body is one FORWARDER_BODY expression; anything else must match
- # ALIAS_SHAPES.
- def offences(file)
- rel = file.relative_path_from(LIB)
- lines = significant_lines(file)
-
- # A semicolon is how several statements -- or an entire
- # `def x; body; end` -- hide inside one "line", which would then be
- # judged as a single line. Never alias-shaped, whatever it says.
- #
- # Scanned over EVERY line up front, not inside the walk below: the walk
- # steps past a def's body line without re-examining it, so a semicolon
- # there went unseen.
- found = lines.filter_map do |line|
- "#{rel}: `#{line}` puts more than one statement on a line" if line.include?(";")
- end
- index = 0
-
- while index < lines.length
- line = lines[index]
-
- if line.start_with?("def ")
- body, terminator = lines[index + 1], lines[index + 2]
- unless FORWARDER_BODY.match?(body.to_s) && terminator == "end"
- found << "#{rel}: `#{line}` is not a single-expression forwarder into SnapDiff"
- end
- index += 3
- else
- found << "#{rel}: unexpected line `#{line}`" unless ALIAS_SHAPES.match?(line)
- index += 1
- end
- end
-
- found
- end
-
- # Strips comments and blanks, and folds trailing-comma continuations back
- # into one logical line (multi-line def_delegators lists, hash literals).
- def significant_lines(file)
- file.read.lines.map(&:strip)
- .reject { |line| line.empty? || line.start_with?("#") }
- .each_with_object([]) do |line, folded|
- if folded.last&.end_with?(",")
- folded[-1] = "#{folded.last} #{line}"
- else
- folded << line
- end
- end
- end
-end
diff --git a/test/legacy/namespace_forwarding_test.rb b/test/legacy/namespace_forwarding_test.rb
deleted file mode 100644
index ce2a8783..00000000
--- a/test/legacy/namespace_forwarding_test.rb
+++ /dev/null
@@ -1,153 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-# The shared harness loads canonical entry points only, so a legacy-surface
-# test pulls in the v1 entry itself -- the require goes with the file in 2.1.
-require "capybara_screenshot_diff"
-
-# Every old-namespace constant touched by the ADR-004 v2 file-tree move
-# must forward to the exact same object as its SnapDiff:: replacement --
-# not a copy, not a lookalike, the same object. If a forwarder ever
-# breaks (wrong target, deleted alias, typo), this fails loudly instead
-# of surfacing as a mysterious downstream `NameError` or a comparison
-# that always returns false.
-#
-# LEGACY SURFACE (test/legacy/, see the Rakefile): deleted with lib/capybara*
-# in 2.1, when there is no old namespace left to forward.
-class NamespaceForwardingTest < ActiveSupport::TestCase
- # This file's whole purpose is resolving the old names, so silence the
- # shims' deprecation warnings here (the suite-wide guard in test_helper
- # raises on any unexpected one); the warning behavior itself is pinned by
- # legacy_namespace_deprecation_test.rb.
- setup do
- @original_silence = SnapDiff.silence_deprecations
- SnapDiff.silence_deprecations = true
- end
-
- teardown do
- SnapDiff.silence_deprecations = @original_silence
- end
-
- # old constant path => new constant path
- MAPPING = {
- "Capybara::Screenshot::Os" => "SnapDiff::Os",
- "Capybara::Screenshot::BrowserHelpers" => "SnapDiff::BrowserHelpers",
- "Capybara::Screenshot::Diff::Vcs" => "SnapDiff::Vcs",
- "Capybara::Screenshot::Diff::VERSION" => "SnapDiff::VERSION",
- "Capybara::Screenshot::Screenshoter" => "SnapDiff::Screenshoter",
- "Capybara::Screenshot::Diff::StableScreenshoter" => "SnapDiff::StableScreenshoter",
- "Capybara::Screenshot::Diff::ImagePreprocessor" => "SnapDiff::ImagePreprocessor",
- "Capybara::Screenshot::Diff::AreaCalculator" => "SnapDiff::AreaCalculator",
- "Capybara::Screenshot::Diff::AnnotationService" => "SnapDiff::AnnotationService",
- "Capybara::Screenshot::Diff::Utils" => "SnapDiff::Utils",
- "Capybara::Screenshot::Diff::ScreenshotMatcher" => "SnapDiff::ScreenshotMatcher",
- "CapybaraScreenshotDiff::DSL" => "SnapDiff::DSL",
- "CapybaraScreenshotDiff::SnapManager" => "SnapDiff::SnapManager",
- "CapybaraScreenshotDiff::Snap" => "SnapDiff::Snap",
- "CapybaraScreenshotDiff::ScreenshotNamer" => "SnapDiff::ScreenshotNamer",
- "CapybaraScreenshotDiff::AttemptsReporter" => "SnapDiff::AttemptsReporter",
- "CapybaraScreenshotDiff::BacktraceFilter" => "SnapDiff::BacktraceFilter",
- "CapybaraScreenshotDiff::ErrorWithFilteredBacktrace" => "SnapDiff::ErrorWithFilteredBacktrace",
- "CapybaraScreenshotDiff::Reporters::HTML" => "SnapDiff::Reporters::HTML",
- "CapybaraScreenshotDiff::ScreenshotAssertion" => "SnapDiff::ScreenshotAssertion",
- "CapybaraScreenshotDiff::AssertionRegistry" => "SnapDiff::AssertionRegistry",
- "Capybara::Screenshot::Diff::Drivers" => "SnapDiff::Drivers",
- "Capybara::Screenshot::Diff::Drivers::BaseDriver" => "SnapDiff::Driver",
- "Capybara::Screenshot::Diff::Drivers::ChunkyPNGDriver" => "SnapDiff::Drivers::ChunkyPNGDriver",
- "Capybara::Screenshot::Diff::Drivers::VipsDriver" => "SnapDiff::Drivers::VipsDriver",
- "Capybara::Screenshot::Diff::ImageCompare" => "SnapDiff::Comparison",
- "Capybara::Screenshot::Diff::Difference" => "SnapDiff::ComparisonResult",
- "CapybaraScreenshotDiff::RED_RGBA" => "SnapDiff::RED_RGBA",
- "CapybaraScreenshotDiff::ORANGE_RGBA" => "SnapDiff::ORANGE_RGBA"
- }.freeze
-
- # Explicit requires: a dedicated forwarder-identity test shouldn't rely
- # on incidental transitive loads from other test files (or on rake's
- # file-load order within a single process) to make every one of these
- # constants resolvable. Most of these are already pulled in by
- # test_helper's own "capybara_screenshot_diff/minitest" require; listed
- # here anyway so this file passes standalone.
- require "capybara/screenshot/diff/os"
- require "capybara/screenshot/diff/browser_helpers"
- require "capybara/screenshot/diff/vcs"
- require "capybara/screenshot/diff/version"
- require "capybara/screenshot/diff/screenshoter"
- require "capybara/screenshot/diff/stable_screenshoter"
- require "capybara/screenshot/diff/image_preprocessor"
- require "capybara/screenshot/diff/area_calculator"
- require "capybara/screenshot/diff/annotation_service"
- require "capybara/screenshot/diff/utils"
- require "capybara/screenshot/diff/screenshot_matcher"
- require "capybara_screenshot_diff/dsl"
- require "capybara_screenshot_diff/snap_manager"
- require "capybara_screenshot_diff/snap"
- require "capybara_screenshot_diff/screenshot_namer"
- require "capybara_screenshot_diff/attempts_reporter"
- require "capybara_screenshot_diff/error_with_filtered_backtrace"
- require "capybara_screenshot_diff/reporters/html"
- require "capybara_screenshot_diff/screenshot_assertion"
- require "capybara/screenshot/diff/reporters/default"
- require "capybara/screenshot/diff/drivers"
- require "capybara/screenshot/diff/drivers/base_driver"
- require "capybara/screenshot/diff/drivers/chunky_png_driver"
- require "capybara/screenshot/diff/image_compare"
- require "capybara/screenshot/diff/difference"
- begin
- require "capybara/screenshot/diff/drivers/vips_driver"
- rescue LoadError, RuntimeError # vips_driver.rb re-raises missing-gem LoadError as RuntimeError
- # vips-less runner: the VipsDriver pair reports as a skip below,
- # mirroring test/unit/drivers/vips_driver_test.rb.
- end
-
- MAPPING.each do |old_name, new_name|
- define_method(:"test_#{old_name}_forwards_to_#{new_name}") do
- skip "vips not available on this runner" if new_name.include?("Vips") && !defined?(SnapDiff::Drivers::VipsDriver)
-
- old_const = Object.const_get(old_name)
- new_const = Object.const_get(new_name)
-
- assert_same new_const, old_const,
- "expected #{old_name} to be the exact same object as #{new_name}, " \
- "got #{old_const.inspect} vs #{new_const.inspect}"
- end
- end
-
- test "MAPPING covers all 29 documented lazy forwarders" do
- assert_equal 29, MAPPING.size
- end
-
- # Documented user-facing constants (beta3 blocker): a subclassing
- # extension point and the images struct. EAGER same-object aliases, not
- # lazy shims, for the same reason as the error classes -- const_defined?
- # and defined? never trigger const_missing, so a lazy shim makes feature
- # detection by the old name silently report "absent", permanently.
- {
- "Capybara::Screenshot::Diff::Reporters::Default" => "SnapDiff::Reporters::Default",
- "Capybara::Screenshot::Diff::Comparison" => "SnapDiff::Comparison::Images"
- }.each do |old_name, new_name|
- test "#{old_name} is an eager same-object alias of #{new_name}" do
- mod, leaf = old_name.rpartition("::").values_at(0, 2)
-
- assert Object.const_get(mod).const_defined?(leaf, false),
- "#{leaf} must be an eagerly-defined constant on #{mod}, not a const_missing shim"
- assert_same Object.const_get(new_name), Object.const_get(old_name)
- end
- end
-
- # Driver registries (ADR-008 step 5b): not part of MAPPING because the
- # old names are EAGER aliases (never warn) of the canonical
- # SnapDiff::Drivers accessors -- LOADED_DRIVERS is mutated in place by
- # user driver registration, so it must stay the exact same object.
- test "driver registries are the same object under old and canonical names" do
- assert_same SnapDiff::Drivers.loaded, Capybara::Screenshot::Diff::LOADED_DRIVERS
- assert_same SnapDiff::Drivers.available, Capybara::Screenshot::Diff::AVAILABLE_DRIVERS
- end
-
- test "driver registration through the legacy LOADED_DRIVERS constant is visible canonically" do
- Capybara::Screenshot::Diff::LOADED_DRIVERS[:forwarding_probe] = :probe_driver
-
- assert_equal :probe_driver, SnapDiff::Drivers.loaded[:forwarding_probe]
- ensure
- SnapDiff::Drivers.loaded.delete(:forwarding_probe)
- end
-end
diff --git a/test/legacy/snap_diff_deprecation_test.rb b/test/legacy/snap_diff_deprecation_test.rb
deleted file mode 100644
index d9685df1..00000000
--- a/test/legacy/snap_diff_deprecation_test.rb
+++ /dev/null
@@ -1,236 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "open3"
-require "snap_diff/deprecation"
-
-# LEGACY SURFACE (test/legacy/, see the Rakefile): SnapDiff::Deprecation is
-# the channel that announces the v1 shims, so snap_diff/deprecation.rb and
-# this file are deleted together with lib/capybara* in 2.1.
-class SnapDiffDeprecationTest < ActiveSupport::TestCase
- def setup
- SnapDiff::Deprecation.reset!
- @original_silence = SnapDiff.silence_deprecations
- @original_env = ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"]
- # These examples assert on the unsilenced default warning behavior, so
- # tell the suite-wide raise-on-deprecation guard (test_helper) that the
- # warnings emitted here -- including from spawned threads -- are expected.
- SnapDiff.silence_deprecations = false
- SnapDiffDeprecationGuard.expected = true
- end
-
- def teardown
- SnapDiffDeprecationGuard.expected = false
- SnapDiff::Deprecation.reset!
- SnapDiff.silence_deprecations = @original_silence
-
- if @original_env.nil?
- ENV.delete("SNAP_DIFF_SILENCE_DEPRECATIONS")
- else
- ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = @original_env
- end
- end
-
- # Emission channel under test: Kernel#warn delegates to Warning.warn
- # (Ruby >= 2.4), whose default implementation writes to $stderr. Capturing
- # $stderr via minitest's capture_io exercises that exact path -- the same
- # one a caller who has customized Warning.warn (e.g. to raise on warnings,
- # or RSpec/Rails deprecation collectors) would also observe -- without us
- # having to monkey-patch Warning ourselves just to assert on it.
- def capture_warnings
- _out, err = capture_io { yield }
- err.lines.reject(&:empty?)
- end
-
- test "warns exactly once for repeated calls with the same subject" do
- lines = capture_warnings do
- 3.times { SnapDiff::Deprecation.warn("Old::Thing", "New::Thing") }
- end
-
- assert_equal 1, lines.size
- assert_match(/\[snap_diff deprecation\]/, lines.first)
- assert_match(/Old::Thing/, lines.first)
- assert_match(/New::Thing/, lines.first)
- end
-
- # Actionable attribution: the first caller frame OUTSIDE the gem's lib
- # dir is named, so users can find the deprecated reference. This test
- # file plays the part of "user code" -- the warning must point here.
- test "warning names the caller's file and line" do
- lines = capture_warnings do
- SnapDiff::Deprecation.warn("Old::Where", "New::Where")
- end
-
- assert_match(/called from #{Regexp.escape(File.expand_path(__FILE__))}:\d+/, lines.first)
- end
-
- test "warns separately for different subjects" do
- lines = capture_warnings do
- SnapDiff::Deprecation.warn("Old::A", "New::A")
- SnapDiff::Deprecation.warn("Old::B", "New::B")
- end
-
- assert_equal 2, lines.size
- end
-
- test "silenced via SnapDiff.silence_deprecations accessor" do
- SnapDiff.silence_deprecations = true
-
- lines = capture_warnings do
- SnapDiff::Deprecation.warn("Old::Thing", "New::Thing")
- end
-
- assert_empty lines
- end
-
- test "not silenced when accessor is explicitly false" do
- SnapDiff.silence_deprecations = false
-
- lines = capture_warnings do
- SnapDiff::Deprecation.warn("Old::Thing", "New::Thing")
- end
-
- assert_equal 1, lines.size
- end
-
- test "silenced via SNAP_DIFF_SILENCE_DEPRECATIONS=1 env var" do
- ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = "1"
-
- lines = capture_warnings do
- SnapDiff::Deprecation.warn("Old::Thing", "New::Thing")
- end
-
- assert_empty lines
- end
-
- test "silenced via SNAP_DIFF_SILENCE_DEPRECATIONS=true env var" do
- ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"] = "true"
-
- lines = capture_warnings do
- SnapDiff::Deprecation.warn("Old::Thing", "New::Thing")
- end
-
- assert_empty lines
- end
-
- test "thread-safe: N threads warning about the same subject emit exactly once" do
- lines = capture_warnings do
- threads = Array.new(20) do
- Thread.new { SnapDiff::Deprecation.warn("Old::Racy", "New::Racy") }
- end
- threads.each(&:join)
- end
-
- assert_equal 1, lines.size
- end
-
- test "reset! clears the seen-set so a subject warns again" do
- capture_warnings { SnapDiff::Deprecation.warn("Old::Thing", "New::Thing") }
-
- SnapDiff::Deprecation.reset!
-
- lines = capture_warnings do
- SnapDiff::Deprecation.warn("Old::Thing", "New::Thing")
- end
-
- assert_equal 1, lines.size
- end
-
- # --- the once-per-process migration notice ---
- #
- # Most of the v1 surface cannot warn per use: the legacy config accessors
- # are plain delegators, and the eagerly-aliased constants (Os, the error
- # classes, VERSION) never reach const_missing. Exercising all 14 legacy
- # APIs a real setup file touches under -w produced ZERO warnings, so a 2.x
- # app was completely silent right up to the bare NameError it would get on
- # 3.0. {MIGRATION_NOTICE} is the one line that closes that gap; these
- # probes run in subprocesses because "once per process" is the contract.
-
- NOTICE_MARKER = "shown once per process"
-
- # Every door into the v1 surface that CAN be hooked, each on its own so a
- # regression in one is not masked by another still firing. (The eagerly
- # aliased constants -- Os, the error classes, VERSION -- are deliberately
- # absent: they never reach const_missing, which is exactly why the notice
- # has to exist and why UPGRADING.md documents them as silent by design.)
- LEGACY_DOORS = {
- "config delegator (write)" => "Capybara::Screenshot.window_size = [80, 80]",
- "config delegator (read)" => "Capybara::Screenshot::Diff.tolerance",
- "const_missing constant" => "Capybara::Screenshot::Diff::ImageCompare",
- "legacy include" => "Class.new { include Capybara::Screenshot::Diff }",
- # Hand-written forwarder, not one of the generated delegators above, so
- # it needs its own Deprecation.notice and its own row here.
- "hand-written derived reader" => "Capybara::Screenshot::Diff.default_options"
- }.freeze
-
- LEGACY_USE = LEGACY_DOORS.values.join("\n")
-
- LEGACY_DOORS.each do |door, code|
- test "the migration notice fires for the #{door}, on its own" do
- out = run_probe(<<~RUBY)
- require "capybara_screenshot_diff"
- 3.times { #{code} }
- RUBY
-
- assert_equal 1, out.scan(NOTICE_MARKER).size, "expected exactly one migration notice, got:\n#{out}"
- end
- end
-
- test "the migration notice fires exactly once per process, however many legacy APIs are used" do
- out = run_probe(<<~RUBY)
- require "capybara_screenshot_diff"
- 3.times do
- #{LEGACY_USE}
- end
- RUBY
-
- assert_equal 1, out.scan(NOTICE_MARKER).size, "expected exactly one migration notice, got:\n#{out}"
- assert_includes out, "docs/UPGRADING.md"
- assert_includes out, "still works in 2.0 and is REMOVED in 2.1"
- assert_includes out, "SNAP_DIFF_SILENCE_DEPRECATIONS"
- end
-
- test "the migration notice does not fire for a purely canonical setup" do
- out = run_probe(<<~RUBY)
- require "snap_diff/integrations/minitest"
- SnapDiff.configure { |c| c.window_size = [80, 80] }
- SnapDiff.config.tolerance
- SnapDiff::Comparison
- SnapDiff::Os
- RUBY
-
- assert_equal "", out.strip, "canonical-only usage must stay silent"
- end
-
- test "the migration notice is silenced by the SnapDiff.silence_deprecations accessor" do
- out = run_probe(<<~RUBY)
- require "capybara_screenshot_diff"
- SnapDiff.silence_deprecations = true
- #{LEGACY_USE}
- RUBY
-
- assert_equal "", out.strip
- end
-
- test "the migration notice is silenced by SNAP_DIFF_SILENCE_DEPRECATIONS" do
- out = run_probe(<<~RUBY, "SNAP_DIFF_SILENCE_DEPRECATIONS" => "1")
- require "capybara_screenshot_diff"
- #{LEGACY_USE}
- RUBY
-
- assert_equal "", out.strip
- end
-
- private
-
- # Runs +script+ in a fresh process with only lib/ on the load path and
- # returns whatever it wrote to stderr, minus warnings from other gems.
- def run_probe(script, env = {})
- project_root = File.expand_path("../..", __dir__)
- _out, err, status = Open3.capture3(
- env, RbConfig.ruby, "-Ilib", "-e", script, chdir: project_root
- )
- assert_predicate status, :success?, err
- err.lines.grep(/\[snap_diff/).join
- end
-end
diff --git a/test/support/driver_contract_tests.rb b/test/support/driver_contract_tests.rb
index 4ebbe9d4..3028ba4d 100644
--- a/test/support/driver_contract_tests.rb
+++ b/test/support/driver_contract_tests.rb
@@ -2,7 +2,21 @@
require "active_support/concern"
-# Shared contract tests for all image processing drivers.
+# The image-backend contract, exercised against SnapDiff::Drivers::VipsDriver.
+#
+# 2.1 removed the driver abstraction, so this is no longer a SHARED contract --
+# there is one includer. It is kept, not folded into vips_driver_test.rb,
+# because what it pins is still worth pinning and is a different KIND of claim
+# from the tests there: those cover vips-specific mechanics (annotation files,
+# class-level mask math, difference regions), while these are the behavioural
+# obligations the rest of the gem relies on -- Comparison, ImagePreprocessor,
+# Screenshoter and AnnotationService all call these methods and would break
+# silently if a signature or a slot order drifted.
+#
+# What did NOT survive the collapse: the `supports?(feature)` capability probe
+# (it existed so ImagePreprocessor could ask whether a driver implemented
+# median filtering -- chunky_png did not, vips does, so the question is gone),
+# and the value of running the same expectations against two implementations.
# Include in any driver test class that uses DSLStub (provides make_comparison).
module DriverContractTests
extend ActiveSupport::Concern
@@ -40,17 +54,19 @@ module DriverContractTests
end
# Method presence / signature --------------------------------------------
- # Pins the current de-facto driver interface so a v2 refactor (inheritance
- # -> mixin, class renames) has a regression net. See dissent #4 in the v2
- # architecture design: method NAMES are intentionally out of scope here.
+ # Pins the interface the rest of the gem calls into. It survived the
+ # inheritance -> mixin -> single-class collapse unchanged, which is the
+ # point: these names are what Comparison, ImagePreprocessor, Screenshoter
+ # and AnnotationService depend on.
- test "[contract] driver implements the shared driver interface" do
+ test "[contract] driver implements the image-backend interface" do
driver = make_comparison(:a, :a).driver
%i[
load_images add_black_box find_difference_region crop from_file
save_image_to resize_image_to draw_rectangles same_pixels?
- same_dimension? height_for width_for image_area_size dimension supports?
+ same_dimension? height_for width_for image_area_size dimension
+ filter_image_with_median
].each do |method_name|
assert_respond_to driver, method_name, "driver should implement ##{method_name}"
end
@@ -139,9 +155,8 @@ module DriverContractTests
end
# Option handling -----------------------------------------------------------
- # tolerance/color_distance_limit/skip_area are supported identically by both
- # drivers today; thresholds below are chosen with headroom on both drivers'
- # actual measurements for the a/b and a/d fixture pairs.
+ # Thresholds below are chosen with headroom on the driver's actual
+ # measurements for the a/b and a/d fixture pairs.
test "[contract] tolerance option treats small differences as equal" do
comp = make_comparison(:a, :b, tolerance: 0.5)
diff --git a/test/support/driver_coverage.rb b/test/support/driver_coverage.rb
deleted file mode 100644
index b6e430d4..00000000
--- a/test/support/driver_coverage.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-# Reports which screenshot-diff drivers were detected as loadable for this run, and
-# guards CI against a driver going silently missing (e.g. libvips not installed).
-#
-# Driver availability is detected once, at load time, via
-# SnapDiff::Drivers.available — so if libvips is missing, vips-gated tests
-# (see test/unit/drivers/vips_driver_test.rb) register and report as skips
-# rather than failing. That's expected on a vips-less runner; this module
-# exists so CI specifically (not a plain dev machine) still fails loudly when
-# a driver it's supposed to have goes missing.
-#
-# Plain top-level module: it is test scaffolding, so it has no business
-# reopening a gem namespace -- least of all the v1 one 3.0 deletes.
-module DriverCoverage
- ALL_DRIVERS = %i[chunky_png vips].freeze
-
- def self.banner(available)
- unavailable = ALL_DRIVERS - available
- msg = "[capybara-screenshot-diff] drivers detected: #{available.join(", ")}"
- msg += " | unavailable: #{unavailable.join(", ")}" if unavailable.any?
- msg
- end
-
- # Drivers CI is missing but expected to have. Empty outside CI, or when an
- # expected driver was explicitly excluded (e.g. a runner that can't install
- # libvips) via the `exclude` list.
- def self.missing_for_ci(available, ci:, exclude: [])
- return [] if ci.to_s.empty?
-
- (ALL_DRIVERS - Array(exclude)) - available
- end
-end
diff --git a/test/system_test_case.rb b/test/system_test_case.rb
index f96d176c..bda94a9f 100644
--- a/test/system_test_case.rb
+++ b/test/system_test_case.rb
@@ -21,7 +21,9 @@ class SystemTestCase < ActiveSupport::TestCase
@orig_save_path = SnapDiff.config.save_path
SnapDiff.config.save_path = "./doc/screenshots"
- SnapDiff.config.driver = ENV.fetch("SCREENSHOT_DRIVER", "chunky_png").to_sym
+ # `SnapDiff.config.driver = ENV.fetch("SCREENSHOT_DRIVER", ...)` is gone
+ # with the setting: 2.1 made libvips the only backend, so there is nothing
+ # for the env var to select.
# TODO: Makes configurations copying and restoring much easier
diff --git a/test/test_helper.rb b/test/test_helper.rb
index 191afe33..1272b6a8 100644
--- a/test/test_helper.rb
+++ b/test/test_helper.rb
@@ -24,60 +24,26 @@
require "snap_diff/integrations/minitest"
-# This suite IS the v1 surface, not a consumer of it: it configures through
-# `Capybara::Screenshot.*` below and exercises the legacy entry points on
-# purpose, so the once-per-process migration notice would fire on every run.
-# Mark it shown rather than silencing deprecations wholesale -- the
-# per-constant warnings must still reach the guard below and raise.
-SnapDiff::Deprecation.suppress_migration_notice!
-
-# Same reasoning for the OTHER half of the 2.1 story: this suite runs the
-# whole comparison matrix on chunky_png, sets shift_distance_limit, and reads
-# the driver registry on purpose -- it exercises those APIs rather than
-# depending on them. Suppressed here, asserted in a subprocess instead
-# (test/unit/removed_in_2_1_deprecation_test.rb), because "once per process"
-# cannot be measured inside one long-lived process anyway.
-SnapDiff::Removal.suppress!
-
-# v2 step 8: the suite exercises only canonical SnapDiff:: names, so any
-# legacy-shim deprecation warning during a test run is a bug in the
-# referencing test -- fail loud at the resolution site instead of letting
-# it scroll by on stderr. Tests that exercise the legacy surface on purpose
-# opt out per test: namespace_forwarding_test silences deprecations in its
-# own setup; the deprecation-machinery tests set `expected = true` around
-# the warnings they capture and assert on. A plain module flag (not a
-# thread-local) so warnings emitted from threads a test spawns are covered
-# too; tests run serially, so there is no cross-test race.
-module SnapDiffDeprecationGuard
- singleton_class.attr_accessor :expected
-
- def warn(message, ...)
- if message.to_s.include?("[snap_diff deprecation]") && !SnapDiffDeprecationGuard.expected
- raise "old-namespace constant resolved inside the test suite: #{message}"
- end
-
- super
- end
-end
-Warning.extend(SnapDiffDeprecationGuard)
+# The deprecation machinery this file used to configure -- the migration
+# notice, the 2.1 removal warnings, and the `Warning` guard that raised on
+# any `[snap_diff deprecation]` line -- went with 2.1. There is no channel
+# left to suppress or to police: a legacy require now fails loudly by
+# itself, and the two static gates (core_tree_has_no_legacy_deps_test,
+# canonical_suite_has_no_legacy_refs_test) catch names that only appear in
+# text.
+#
+# The DriverCoverage banner/abort went the same way. It guarded against a
+# SILENT fallback to chunky_png when libvips was missing; with one backend a
+# missing libvips is a `require "vips"` LoadError at boot, not a quiet
+# downgrade.
require "support/stub_test_methods"
require "support/setup_capybara_drivers"
require "support/test_helpers"
-require "support/driver_coverage"
SnapDiff.config.root = Rails.root
SnapDiff.config.save_path = "./doc/screenshots"
-puts DriverCoverage.banner(SnapDiff::Drivers.available)
-
-missing_drivers = DriverCoverage.missing_for_ci(
- SnapDiff::Drivers.available,
- ci: ENV["CI"],
- exclude: ENV["CI_EXPECTED_DRIVERS_EXCLUDE"]&.split(",")&.map(&:to_sym)
-)
-abort("[capybara-screenshot-diff] CI is missing expected driver(s): #{missing_drivers.join(", ")}") if missing_drivers.any?
-
class ActiveSupport::TestCase
include TestHelpers::Assertions
include TestHelpers::DriverSetup
diff --git a/test/unit/attempts_reporter_test.rb b/test/unit/attempts_reporter_test.rb
index c2fb19fc..1333ef1d 100644
--- a/test/unit/attempts_reporter_test.rb
+++ b/test/unit/attempts_reporter_test.rb
@@ -27,7 +27,7 @@ class AttemptsReporterTest < ActiveSupport::TestCase
test "#generate returns the timeout message listing every attempt artifact" do
snap = attempt_snapshot("unstable_message", %i[a b])
- message = AttemptsReporter.new(snap, {driver: :chunky_png}, {wait: 2, stability_time_limit: 0.1}).generate
+ message = AttemptsReporter.new(snap, {}, {wait: 2, stability_time_limit: 0.1}).generate
assert_match(/Could not get stable screenshot within 2s/, message)
snap.find_attempts_paths.each do |attempt_path|
@@ -40,7 +40,7 @@ class AttemptsReporterTest < ActiveSupport::TestCase
newest_attempt = Pathname.new(snap.find_attempts_paths.max)
original_bytes = newest_attempt.binread
- AttemptsReporter.new(snap, {driver: :chunky_png}, {wait: 2, stability_time_limit: 0.1}).generate
+ AttemptsReporter.new(snap, {}, {wait: 2, stability_time_limit: 0.1}).generate
assert_predicate newest_attempt, :exist?
assert_not_equal original_bytes, newest_attempt.binread,
@@ -65,7 +65,7 @@ def take_screenshot(screenshot_path)
SnapDiff.config.stub(:screenshoter, alternating_screenshoter) do
error = assert_raises(SnapDiff::UnstableImage) do
StableScreenshoter
- .new({stability_time_limit: 0.05, wait: 0.2}, {driver: :chunky_png})
+ .new({stability_time_limit: 0.05, wait: 0.2}, {})
.take_comparison_screenshot(snap)
end
end
diff --git a/test/unit/canonical_suite_has_no_legacy_refs_test.rb b/test/unit/canonical_suite_has_no_legacy_refs_test.rb
index 6d81df19..5c6e376c 100644
--- a/test/unit/canonical_suite_has_no_legacy_refs_test.rb
+++ b/test/unit/canonical_suite_has_no_legacy_refs_test.rb
@@ -5,18 +5,16 @@
# The TEST-TREE half of the split that core_tree_has_no_legacy_deps_test.rb
# guards for lib/.
#
-# `rake test:canonical` is defined as "exactly what must still pass once
-# test/legacy/ and the v1 trees are gone". A test that ASSERTS legacy
-# behaviour from test/unit/ or test/integration/ is therefore a time bomb:
-# it passes today and fails the day the deletion lands, long after its author
-# has moved on. That has now happened three times in review -- a canonical
-# surface table demanding a shim-only method, legacy-constant probes written
-# into a canonical file, an umbrella guard that had to be relocated. Reviews
-# caught all three; this catches the fourth.
+# Before 2.1 this gate protected a FUTURE deletion: a test asserting legacy
+# behaviour passed then and would fail the day the v1 trees went. The deletion
+# has landed, so a legacy `require` now fails by itself and this gate no longer
+# has to predict anything. It stays for the half that still does not fail on
+# its own -- a legacy NAME in a string, a heredoc-embedded subprocess script,
+# an assertion message or a docstring. Those survive the deletion and start
+# lying the moment it happens.
#
-# Scope: test/unit/ and test/integration/ -- the two trees `test:canonical`
-# runs and the deletion keeps. test/legacy/ is deliberately NOT policed: its
-# whole job is to exercise the legacy surface, and it is deleted with it.
+# Scope: test/unit/ and test/integration/ -- the whole suite now that
+# test/legacy/ is gone.
#
# WHOLE-LINE comments are ignored, same as the twin gate: a comment
# explaining that a forwarder used to live under the old name is history, not
@@ -32,11 +30,20 @@ class CanonicalSuiteHasNoLegacyRefsTest < ActiveSupport::TestCase
# allowlist, whose text is again an offence -- no fixed point exists.
SELF = Pathname.new(File.expand_path(__FILE__))
+ # The compat surface's own test, excluded whole for the same reason as SELF.
+ # Since the ADR-008 amendment (2026-08-24) the v1 NAMES are a kept, permanent
+ # alias surface -- so one test has to exercise them, written as the affected
+ # user's own code. A line allowlist for a file whose every line is the
+ # subject buys nothing; the gate still holds for every other test file.
+ COMPAT_SURFACE_TESTS = ["unit/compat_surface_test.rb"].freeze
+
+ EXCLUDED = ([SELF] + COMPAT_SURFACE_TESTS.map { |rel| TEST_ROOT.join(rel) }).freeze
+
ALL_FILES = (
Dir[TEST_ROOT.join("unit/**/*_test.rb")] + Dir[TEST_ROOT.join("integration/**/*_test.rb")]
).map { |path| Pathname.new(path) }.sort.freeze
- CANONICAL_FILES = ALL_FILES.reject { |file| file == SELF }.freeze
+ CANONICAL_FILES = ALL_FILES.reject { |file| EXCLUDED.include?(file) }.freeze
# A require of a doomed path: the v1 trees (`capybara/screenshot/...`,
# `capybara_screenshot_diff...`, the `capybara-screenshot-diff` gem-name
@@ -56,20 +63,24 @@ class CanonicalSuiteHasNoLegacyRefsTest < ActiveSupport::TestCase
# A read or write of a v1 namespace constant.
LEGACY_CONSTANT = /(? [
- '["snap_diff.rb", %(require "snap_diff/legacy_shims"), nil],',
- '%(require "capybara_screenshot_diff/minitest"),',
- 'gate << "SnapDiff.start is still defined" if SnapDiff.respond_to?(:start)',
- 'gate << "CapybaraScreenshotDiff is still defined" if defined?(CapybaraScreenshotDiff)',
- 'assert_includes failure, "SnapDiff.start is still defined"'
- ],
- # The twin gate's own pattern literal. A detector has to spell what it
- # detects; the line asserts nothing about legacy behaviour and is deleted
- # with the trees it guards.
+ # The twin gate's own pattern literals. A detector has to spell what it
+ # detects; these lines assert nothing and scan lib/, not the suite.
"unit/core_tree_has_no_legacy_deps_test.rb" => [
- 'LEGACY_CONSTANT = /(? [
+ "SnapDiff::Deprecation",
+ "SnapDiff::Removal",
+ "SnapDiff::Driver",
+ "SnapDiff::Drivers::AVAILABLE_DRIVERS",
+ "REMOVED_METHODS = %w[start silence_deprecations silence_deprecations=].freeze"
]
}.freeze
test "no canonical test references the legacy namespaces or entry points" do
refute_empty CANONICAL_FILES, "canonical glob matched nothing -- the gate would pass vacuously"
- assert_equal ALL_FILES.size - 1, CANONICAL_FILES.size,
- "SELF no longer names a scanned file, so this gate is scanning itself or nothing"
+ assert_equal ALL_FILES.size - EXCLUDED.size, CANONICAL_FILES.size,
+ "an exclusion no longer names a scanned file, so this gate is scanning itself or nothing"
+ EXCLUDED.each { |file| assert file.exist?, "#{file} is excluded from this gate but does not exist" }
offenders = CANONICAL_FILES.flat_map { |file| offences(file) }
assert_empty offenders, <<~MSG
- Canonical test(s) assert legacy behaviour. `rake test:canonical` is what
- must still pass once test/legacy/ and the v1 trees are deleted, so these
- lines are green today and red the day the deletion lands.
-
- Move the test to test/legacy/ (it dies with what it tests), or rewrite it
- against the canonical surface (`SnapDiff.config.*`, `SnapDiff::Os`,
- `SnapDiff::Minitest::Assertions`, `require "snap_diff/..."`):
+ Test(s) name a surface 2.1 removed. The v1 trees, the deprecation
+ channel and the driver abstraction are gone, so these names resolve to
+ nothing -- and where they sit in a string or a docstring, nothing else
+ will say so.
+
+ Rewrite against what the gem actually has (`SnapDiff.config.*`,
+ `SnapDiff.configure`, `SnapDiff::Os`, `SnapDiff::Minitest::Assertions`,
+ `SnapDiff::Drivers::VipsDriver`, `require "snap_diff/..."`):
#{offenders.join("\n")}
MSG
diff --git a/test/unit/compare_api_test.rb b/test/unit/compare_api_test.rb
index 811dbf97..d768e273 100644
--- a/test/unit/compare_api_test.rb
+++ b/test/unit/compare_api_test.rb
@@ -32,24 +32,17 @@ class CompareApiTest < ActiveSupport::TestCase
assert result.different?
end
- test ".compare accepts driver option" do
- skip "VIPS not present" unless defined?(Vips)
-
- result = SnapDiff.compare(
- TEST_IMAGES_DIR / "a.png",
- TEST_IMAGES_DIR / "a.png",
- driver: :vips
- )
+ # ".compare accepts driver option" is gone with the option: 2.1 removed
+ # driver selection, so there is nothing left to accept.
+ test ".compare compares two images with the configured defaults" do
+ result = SnapDiff.compare(TEST_IMAGES_DIR / "a.png", TEST_IMAGES_DIR / "a.png")
assert result.quick_equal?
end
test ".compare accepts tolerance options" do
- skip "VIPS not present" unless defined?(Vips)
-
result = SnapDiff.compare(
TEST_IMAGES_DIR / "a.png",
TEST_IMAGES_DIR / "b.png",
- driver: :vips,
tolerance: 1.0
)
assert_not result.different?
diff --git a/test/unit/compat_surface_test.rb b/test/unit/compat_surface_test.rb
new file mode 100644
index 00000000..7cf95816
--- /dev/null
+++ b/test/unit/compat_surface_test.rb
@@ -0,0 +1,375 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "open3"
+# .probe runs a require in a fresh process with only lib/ on the load path.
+# Reused rather than copied so the two suites cannot drift apart.
+require_relative "support_load_probe_test"
+
+# THE 2.1 SAFETY NET, WRITTEN AS THE USER'S CODE.
+#
+# ADR-008 said "no demand" for the v1 names, the `driver` setting and
+# `shift_distance_limit`. Research on 2026-08-24 falsified it against the live
+# GitHub API (.ai/adr-008-amendment-demand-falsified.md):
+#
+# - `CapybaraScreenshotDiff::DSL` / `::Minitest::Assertions` is the entry
+# point 4 of 6 discoverable real users already migrated TO. Zero are on
+# `SnapDiff::*`.
+# - 3 of 6 real configs set `driver` explicitly; two set it to `:vips` --
+# asking for the only backend that survives.
+# - `shift_distance_limit`'s one known user guards the writer with
+# `respond_to?`, so plain deletion is SILENT for them: no error, no
+# warning, tolerance quietly gone. That is the worst possible removal.
+#
+# So 2.1 keeps the vips-only simplification and changes the FAILURE MODE: the
+# names users import survive as permanent aliases, and every seam that is
+# really gone raises with a message naming the replacement.
+#
+# Every assertion below is written the way the affected user wrote it -- their
+# `require`, their constant, their assignment -- deliberately NOT as an
+# internal unit test of SnapDiff::Config. The whole bug being fixed is that
+# the internal tests were green while the user-facing imports broke.
+#
+# This file is excluded WHOLE from canonical_suite_has_no_legacy_refs_test:
+# its entire subject is the v1 alias surface, so a line allowlist for a file
+# whose every line is the subject buys nothing. The gate still holds for the
+# rest of test/unit and test/integration.
+class CompatSurfaceTest < ActiveSupport::TestCase
+ # --- ITEM 1: the names users actually import survive -----------------
+ #
+ # EAGER same-object aliases, not lazy `const_missing` shims. The
+ # distinction is load-bearing and has already produced one false CHANGELOG
+ # claim in this project: `const_defined?` and `defined?` never trigger
+ # `const_missing`, so a lazy shim breaks every adopter that feature-detects
+ # before including.
+
+ test "CapybaraScreenshotDiff::DSL is the same object as SnapDiff::DSL" do
+ assert_same SnapDiff::DSL, CapybaraScreenshotDiff::DSL
+ end
+
+ test "CapybaraScreenshotDiff::Minitest::Assertions is the same object as the canonical one" do
+ assert_same SnapDiff::Minitest::Assertions, CapybaraScreenshotDiff::Minitest::Assertions
+ end
+
+ test "Capybara::Screenshot::Diff is the same object as SnapDiff" do
+ assert_same SnapDiff, Capybara::Screenshot::Diff
+ end
+
+ # The eager-vs-lazy contract, asserted the way adopters check it.
+ test "the alias constants answer const_defined? and defined? without being resolved first" do
+ %w[
+ CapybaraScreenshotDiff
+ CapybaraScreenshotDiff::DSL
+ CapybaraScreenshotDiff::Minitest::Assertions
+ Capybara::Screenshot::Diff
+ ].each do |name|
+ assert Object.const_defined?(name),
+ "#{name} is not const_defined? -- a lazy const_missing shim, not an eager alias"
+ end
+
+ assert defined?(CapybaraScreenshotDiff::DSL)
+ assert defined?(Capybara::Screenshot::Diff)
+ end
+
+ # "call what they call": the alias has to carry the DSL methods, not just
+ # resolve to a module object.
+ test "the aliased DSL carries the assertion methods a user includes it for" do
+ assert_includes CapybaraScreenshotDiff::DSL.instance_methods, :assert_matches_screenshot
+ assert_includes CapybaraScreenshotDiff::DSL.instance_methods, :screenshot
+ assert_includes CapybaraScreenshotDiff::Minitest::Assertions.instance_methods, :assert_matches_screenshot
+ end
+
+ # The require lines the six real configs actually have at the top of their
+ # support files. A LoadError here is the first thing they would hit -- before
+ # any constant above could help them.
+ LEGACY_REQUIRES = {
+ "capybara_screenshot_diff/minitest" => %w[CapybaraScreenshotDiff::DSL CapybaraScreenshotDiff::Minitest::Assertions],
+ "capybara_screenshot_diff/rspec" => %w[CapybaraScreenshotDiff::DSL],
+ "capybara_screenshot_diff/dsl" => %w[CapybaraScreenshotDiff::DSL],
+ "capybara_screenshot_diff" => %w[CapybaraScreenshotDiff],
+ "capybara/screenshot/diff" => %w[Capybara::Screenshot::Diff CapybaraScreenshotDiff::DSL]
+ }.freeze
+
+ test "every require line the real configs use still loads and defines what they name" do
+ failures = LEGACY_REQUIRES.filter_map do |entry, constants|
+ SupportLoadProbeTest.probe(entry, <<~RUBY)
+ require #{entry.inspect}
+ missing = #{constants.inspect}.reject { |c| Object.const_defined?(c) }
+ abort("missing: \#{missing.join(", ")}") unless missing.empty?
+ RUBY
+ end
+
+ assert_empty failures, <<~MSG
+ A require line real users have at the top of their support file no
+ longer loads, or no longer defines the constant they name after it:
+
+ #{failures.join("\n")}
+ MSG
+ end
+
+ # --- ITEM 2: `driver` is accept-and-ignore, except chunky_png --------
+
+ test "setting driver to vips is a silent no-op through the name real configs use" do
+ assert_silent { Capybara::Screenshot::Diff.driver = :vips }
+ assert_equal :vips, Capybara::Screenshot::Diff.driver
+ end
+
+ test "setting driver to auto is accepted and ignored" do
+ assert_silent { SnapDiff.config.driver = :auto }
+ assert_equal :vips, SnapDiff.config.driver
+ end
+
+ # bootstrap_form (1,643 stars) has exactly this line, and its CI installs no
+ # libvips. This is the message that has to tell them what to do.
+ test "setting driver to chunky_png raises and names libvips, ruby-vips and the upgrade doc" do
+ error = assert_raises(ArgumentError) do
+ Capybara::Screenshot::Diff.driver = ENV.fetch("SCREENSHOT_DRIVER", "chunky_png").to_sym
+ end
+
+ assert_match(/chunky_png/, error.message)
+ assert_match(/libvips/, error.message)
+ assert_match(/ruby-vips/, error.message)
+ assert_match(%r{docs/UPGRADING\.md}, error.message)
+ end
+
+ test "the chunky_png rejection does not depend on the value being a symbol" do
+ assert_raises(ArgumentError) { SnapDiff.config.driver = "chunky_png" }
+ end
+
+ # --- ITEM 3: shift_distance_limit raises instead of vanishing --------
+
+ # potlift8 writes exactly this guard, which is why plain deletion is silent
+ # for them: `respond_to?` goes false and the whole line evaporates.
+ test "the shift_distance_limit writer still answers respond_to?" do
+ assert Capybara::Screenshot::Diff.respond_to?(:shift_distance_limit=),
+ "the respond_to?-guarded writer vanished -- removal is silent for its one known user"
+ assert SnapDiff.config.respond_to?(:shift_distance_limit=)
+ end
+
+ test "setting shift_distance_limit raises and names what to use instead" do
+ error = assert_raises(ArgumentError) do
+ Capybara::Screenshot::Diff.shift_distance_limit = 1 if
+ Capybara::Screenshot::Diff.respond_to?(:shift_distance_limit=)
+ end
+
+ assert_match(/shift_distance_limit/, error.message)
+ assert_match(/tolerance/, error.message)
+ assert_match(/color_distance_limit/, error.message)
+ assert_match(%r{docs/UPGRADING\.md}, error.message)
+ end
+
+ test "setting shift_distance_limit on the config object raises too" do
+ assert_raises(ArgumentError) { SnapDiff.config.shift_distance_limit = 1 }
+ end
+
+ # --- ITEM 3, THE CLASS: per-comparison options ----------------------
+ #
+ # The settings above are only half the surface a user can write. The other
+ # half is the per-screenshot options hash, which Comparison froze and never
+ # validated -- so `screenshot "home", shift_distance_limit: 5` was a SILENT
+ # no-op for exactly the same reason the writer was. One guard at the funnel
+ # every option hash passes through, not one per call site.
+
+ test "a removed option in the per-comparison hash raises rather than being ignored" do
+ images = TEST_IMAGES_DIR.join("a.png").to_s
+
+ assert_raises(ArgumentError) { SnapDiff::Comparison.new(images, images, shift_distance_limit: 5) }
+ assert_raises(ArgumentError) { SnapDiff::Comparison.new(images, images, driver: :chunky_png) }
+ end
+
+ test "a per-comparison driver option that names the surviving backend is ignored silently" do
+ images = TEST_IMAGES_DIR.join("a.png").to_s
+
+ assert_silent { SnapDiff::Comparison.new(images, images, driver: :vips) }
+ end
+
+ # The defaults every comparison carries must not trip the guard above --
+ # a false positive here breaks every user at once.
+ test "the default options a normal comparison carries trip no removed-option guard" do
+ images = TEST_IMAGES_DIR.join("a.png").to_s
+
+ assert_silent { SnapDiff::Comparison.new(images, images, SnapDiff.config.default_options) }
+ end
+
+ # --- ITEM 5: the dual-install guard's load-bearing assumption --------
+ #
+ # SnapDiff.assert_single_gem! reads Gem.loaded_specs. snap_diff_test.rb
+ # proves the predicate fires on a two-gem hash and stays quiet on a one-gem
+ # one -- but both hand it a hash, so neither proves the REAL source is
+ # populated the way the guard assumes.
+ #
+ # Measured 2026-08-24 with two path-sourced gems shipping identical files:
+ # both names appear in Gem.loaded_specs with neither ever required, and only
+ # one appears when only one is in the Gemfile. The assertion below is that
+ # experiment reduced to something this suite can keep running: `standard` is
+ # in the bundle, is never required by the suite, and is in loaded_specs
+ # anyway. Bundle membership, not `require`, is what populates it.
+ test "Bundler populates Gem.loaded_specs from bundle membership, not from require" do
+ skip "not running under Bundler" unless defined?(Bundler) && Gem.loaded_specs.key?("capybara-screenshot-diff")
+
+ assert Gem.loaded_specs.key?("standard"),
+ "a bundled gem is missing from Gem.loaded_specs -- assert_single_gem! cannot see a second gem either"
+ assert_empty $LOADED_FEATURES.grep(%r{/standard/base\.rb\z}),
+ "standard got required after all, so this proves nothing about un-required bundle members"
+
+ # ...and the guard's own negative: the second gem name is NOT in this
+ # bundle, so the real Gem.loaded_specs must not trip it.
+ assert_not Gem.loaded_specs.key?("snap_diff-capybara")
+ assert_nil SnapDiff.assert_single_gem!
+ end
+
+ # --- ITEM 1b: the settings those imports actually CALL ---------------
+ #
+ # The names users IMPORT and the names they CALL are different surfaces, and
+ # aliasing only the first is worse than aliasing neither: the constant
+ # resolves, the user believes they are fine, and the next line explodes. Four
+ # of six known real configs write `Capybara::Screenshot::Diff.tolerance=` or
+ # a sibling. Since `Capybara::Screenshot::Diff` IS `SnapDiff`, the config
+ # surface either exists on both or on neither -- there is no partial version.
+
+ test "a v1 setting written through the old namespace lands in the one storage" do
+ Capybara::Screenshot::Diff.tolerance = 0.05
+
+ assert_in_delta 0.05, SnapDiff.config.tolerance
+ end
+
+ test "a setting written canonically is visible through the old namespace" do
+ SnapDiff.config.color_distance_limit = 15
+
+ assert_equal 15, Capybara::Screenshot::Diff.color_distance_limit
+ end
+
+ test "the capture-side holder writes the same storage too" do
+ Capybara::Screenshot.window_size = [1400, 1400]
+
+ assert_equal [1400, 1400], SnapDiff.config.window_size
+ end
+
+ # The one name that is NOT identity-mapped. `Capybara::Screenshot.enabled`
+ # and `Capybara::Screenshot::Diff.enabled` were always two independent
+ # settings that happened to share a bare name under their own modules; a flat
+ # Config cannot expose two attributes called `enabled`, so the capture-side
+ # one became `screenshot_enabled`. Collapsing them would change `active?`.
+ test "enabled stays two different settings, one per holder" do
+ Capybara::Screenshot.enabled = false
+ Capybara::Screenshot::Diff.enabled = true
+
+ assert_equal false, SnapDiff.config.screenshot_enabled
+ assert_equal true, SnapDiff.config.enabled
+ assert_equal false, Capybara::Screenshot.enabled
+ assert_equal true, Capybara::Screenshot::Diff.enabled
+ end
+
+ # `mattr_accessor` defined instance methods as well as singleton ones, and
+ # `include Capybara::Screenshot::Diff` (bootstrap_form has that line) is how
+ # they were reached. An include that silently adds nothing is the same class
+ # of failure as a setter that silently does nothing.
+ test "including the old namespace still brings the settings in as instance methods" do
+ SnapDiff.config.tolerance = 0.02
+ host = Class.new { include Capybara::Screenshot::Diff }.new
+
+ assert_in_delta 0.02, host.tolerance
+
+ host.tolerance = 0.04
+ assert_in_delta 0.04, SnapDiff.config.tolerance
+ end
+
+ # THE DRIFT GUARD. The accessors are generated from Config::SETTINGS, so a
+ # new setting cannot be forgotten -- unless the generation itself is removed
+ # or narrowed. This is what notices that.
+ test "every Config setting is reachable through both v1 holders, on the module and on instances" do
+ holders = [Capybara::Screenshot, Capybara::Screenshot::Diff]
+
+ missing = SnapDiff::Config::SETTINGS.flat_map do |attr|
+ holders.flat_map do |holder|
+ [
+ ["#{holder}.#{attr}", holder.respond_to?(attr)],
+ ["#{holder}.#{attr}=", holder.respond_to?(:"#{attr}=")],
+ ["#{holder}##{attr}", holder.method_defined?(attr)],
+ ["#{holder}##{attr}=", holder.method_defined?(:"#{attr}=")]
+ ].reject { |_name, present| present }.map(&:first)
+ end
+ end
+
+ assert_empty missing, <<~MSG
+ A setting exists on SnapDiff::Config but is unreachable through the v1
+ holders. Real configs write these names; a missing one is a NoMethodError
+ on line 1 of someone's test helper:
+
+ #{missing.join("\n")}
+ MSG
+ end
+
+ # The v1 two-block-arg form. Before the aliases this was a `NameError` --
+ # loud. With `Capybara::Screenshot::Diff = SnapDiff` it would otherwise have
+ # become a one-arg yield, handing the user `nil` for `diff` and a
+ # NoMethodError three lines later. Both holders collapsed into one object, so
+ # it yields that object twice and the old shape keeps working.
+ test "the v1 two-holder configure block still works" do
+ Capybara::Screenshot::Diff.configure do |screenshot, diff|
+ screenshot.window_size = [800, 600]
+ diff.tolerance = 0.003
+ end
+
+ assert_equal [800, 600], SnapDiff.config.window_size
+ assert_in_delta 0.003, SnapDiff.config.tolerance
+ end
+
+ test "the canonical one-argument configure block is unaffected" do
+ SnapDiff.configure { |config| config.tolerance = 0.007 }
+
+ assert_in_delta 0.007, SnapDiff.config.tolerance
+ end
+
+ # --- ITEM 2b: the v1 error name in a rescue clause -------------------
+ #
+ # The worst variety of latent NameError: it fires only when an exception is
+ # already in flight, converting someone's real failure into a confusing one
+ # at the moment they can least afford it.
+ test "rescuing by the v1 error name catches what the gem raises" do
+ assert_same SnapDiff::Error, CapybaraScreenshotDiff::CapybaraScreenshotDiffError
+ assert Object.const_defined?("CapybaraScreenshotDiff::CapybaraScreenshotDiffError")
+
+ caught = begin
+ raise SnapDiff::ExpectationNotMet, "boom"
+ rescue CapybaraScreenshotDiff::CapybaraScreenshotDiffError => e
+ e
+ end
+
+ assert_equal "boom", caught.message
+ end
+
+ # Discovered, not listed: a future error class added under SnapDiff is
+ # reachable under the v1 namespace automatically (same module), and this says
+ # so out loud rather than leaving it to be assumed.
+ test "every error class the gem raises is reachable under the v1 namespace" do
+ unreachable = SnapDiff.constants.filter_map { |name|
+ value = SnapDiff.const_get(name)
+ next unless value.is_a?(Class) && value <= SnapDiff::Error
+
+ "CapybaraScreenshotDiff::#{name}" unless CapybaraScreenshotDiff.const_defined?(name)
+ }
+
+ assert_empty unreachable, "error class(es) not reachable under the v1 namespace: #{unreachable.join(", ")}"
+ end
+
+ # --- The six real configs, replayed verbatim -------------------------
+ #
+ # Not a unit test of anything: these are the actual lines from the
+ # discoverable third-party setups, and the only claim is that a 2.1 process
+ # survives running them.
+ test "the discoverable real-world configs load without raising" do
+ # jaynetics/activeadmin_assets, spec/support/capybara_setup.rb
+ Capybara::Screenshot::Diff.driver = :vips
+ Capybara::Screenshot::Diff.tolerance = 0.05
+ Capybara::Screenshot::Diff.fail_if_new = !ENV["CI"].nil?
+
+ # showca-se/showcase, test/support/system/setup_capybara_screenshot_diff.rb
+ Capybara::Screenshot::Diff.driver = :vips
+ Capybara::Screenshot.screenshot_format = :webp
+ Capybara::Screenshot::Diff.color_distance_limit = 15
+
+ assert_equal :webp, SnapDiff.config.screenshot_format
+ assert_equal 15, SnapDiff.config.color_distance_limit
+ end
+end
diff --git a/test/unit/config_default_timing_test.rb b/test/unit/config_default_timing_test.rb
index 1667dbb1..5735c38c 100644
--- a/test/unit/config_default_timing_test.rb
+++ b/test/unit/config_default_timing_test.rb
@@ -80,9 +80,7 @@ def check(name, expected, actual)
fail_on_difference: true,
color_distance_limit: nil,
enabled: true,
- shift_distance_limit: nil,
skip_area: nil,
- driver: :auto,
tolerance: nil,
perceptual_threshold: nil,
screenshoter: SnapDiff::Screenshoter,
diff --git a/test/unit/core_tree_has_no_legacy_deps_test.rb b/test/unit/core_tree_has_no_legacy_deps_test.rb
index 57718b62..2b0971f0 100644
--- a/test/unit/core_tree_has_no_legacy_deps_test.rb
+++ b/test/unit/core_tree_has_no_legacy_deps_test.rb
@@ -2,20 +2,21 @@
require "test_helper"
-# The REVERSE of legacy_tree_is_alias_only_test.rb, and the other half of
-# what makes the 2.1 deletion a `git rm`.
+# Keeps the removed names from creeping back into lib/.
#
-# That test proves the v1 trees hold no logic. This one proves the canonical
-# core does not reach BACK into them -- which is the half that actually
-# breaks the gem if it is wrong: as long as any file under lib/snap_diff/
-# requires a `capybara/...` path or reads a `Capybara::Screenshot.*` /
-# `CapybaraScreenshotDiff::*` constant, deleting lib/capybara* leaves a core
-# that no longer loads.
+# Its twin (legacy_tree_is_alias_only_test.rb) proved the v1 trees held no
+# logic; the trees are gone and so is that test. This one used to prove the
+# core did not reach BACK into them, which is what made the deletion a
+# `git rm`. The deletion has happened -- a legacy `require` in lib/ now fails
+# loudly on its own -- so the surviving job is the quiet half: a NAME in a
+# string, a user-facing message or a docstring that mentions
+# `Capybara::Screenshot.*`, `SnapDiff::Drivers.available` or
+# `shift_distance_limit` still parses fine and simply lies.
#
-# Scope: everything the 2.1 deletion KEEPS. Files that are themselves part of
-# the deletion set (DELETED_WITH_LEGACY_TREES below) live under lib/snap_diff/ only
-# because the generator for the v1 surface has to be code, and the v1 trees
-# have to stay alias-only -- they are legacy by design and go with it.
+# Scope: every file under lib/ except snap_diff/compat.rb, which IS the
+# compatibility surface (see COMPAT_SURFACE below). The two files that used to
+# BUILD the v1 surface -- legacy_shims.rb, deprecation.rb -- are not excluded;
+# they were deleted.
#
# WHOLE-LINE comments are ignored: "ex +SnapDiff.config.active?+" on its
# own line is history, not a dependency. Everything else on a code line
@@ -27,17 +28,19 @@
class CoreTreeHasNoLegacyDepsTest < ActiveSupport::TestCase
LIB = Pathname.new(__dir__).join("../../lib").expand_path
- # Deleted alongside lib/capybara* in 2.1: these files exist to BUILD the
- # v1 compatibility surface (const_missing shims, the legacy config
- # accessor generator, the deprecation channel that announces both).
- DELETED_WITH_LEGACY_TREES = %w[
- snap_diff/legacy_shims.rb
- snap_diff/deprecation.rb
- ].freeze
+ # EXACTLY ONE ENTRY, and adding a second needs an ADR update rather than a
+ # green build. snap_diff/compat.rb IS the compatibility surface (ADR-008
+ # amendment, 2026-08-24): the permanent v1 name aliases and the raising
+ # stubs for `driver` / `shift_distance_limit`. Naming the old names is its
+ # whole job, so scanning it would only ever produce noise -- but it is one
+ # file, alias-and-message only, and compat_surface_test pins its behaviour.
+ # Every other file under lib/ is still scanned, which is where this gate
+ # earns its keep.
+ COMPAT_SURFACE = ["snap_diff/compat.rb"].freeze
CORE_FILES = (
[LIB.join("snap_diff.rb")] + Dir[LIB.join("snap_diff/**/*.rb")].map { |p| Pathname.new(p) }
- ).sort.reject { |file| DELETED_WITH_LEGACY_TREES.include?(file.relative_path_from(LIB).to_s) }.freeze
+ ).sort.reject { |file| COMPAT_SURFACE.include?(file.relative_path_from(LIB).to_s) }.freeze
# A require of anything in the v1 trees: `capybara/screenshot/...`,
# `capybara_screenshot_diff...`, `capybara-screenshot-diff`. Plain
@@ -53,6 +56,20 @@ class CoreTreeHasNoLegacyDepsTest < ActiveSupport::TestCase
# A read or write of a v1 namespace constant.
LEGACY_CONSTANT = /(?=, 25, "the core glob collapsed -- the gate would barely scan anything"
+ end
+
test "the allowlist names only lines that still exist" do
stale = ALLOWED.flat_map do |path, lines|
file = LIB.join(path)
@@ -113,6 +142,7 @@ def offences(file)
reason =
if LEGACY_REQUIRE.match?(line) then "requires a v1 tree path"
elsif LEGACY_CONSTANT.match?(line) then "references a v1 namespace constant"
+ elsif REMOVED_SURFACE.match?(line) then "names a surface 2.1 removed"
end
"#{rel}:#{number}: #{reason} -- `#{line}`" if reason
end
diff --git a/test/unit/diff_test.rb b/test/unit/diff_test.rb
index bbafabe1..3aa1d694 100644
--- a/test/unit/diff_test.rb
+++ b/test/unit/diff_test.rb
@@ -113,12 +113,11 @@ class DiffTest < ActiveSupport::TestCase
assert_equal "b/00_a", build_full_name(:a)
end
- test "detects available diff drivers" do
- # NOTE for tests we are loading both drivers, so we expect that all of them are available
- expected_drivers = defined?(Vips) ? %i[vips chunky_png] : %i[chunky_png]
-
- assert_equal expected_drivers, SnapDiff::Drivers::AVAILABLE_DRIVERS
- end
+ # "detects available diff drivers" is gone with driver detection itself:
+ # 2.1 removed SnapDiff::Drivers::AVAILABLE_DRIVERS along with the rest of the
+ # abstraction. There is nothing to detect when there is one backend, and
+ # `ruby-vips` is a gemspec runtime dependency, so its absence is a resolver
+ # error rather than a list this gem has to compute.
test "aggregates failures on teardown for Minitest" do
test_case = SampleMiniTestCase.new(:_test_sample_screenshot_error)
@@ -212,7 +211,7 @@ class ScreenshotFormatTest < ActiveSupport::TestCase
set_test_images(snap, :a, :a)
SnapDiff.config.stub(:screenshot_format, "webp") do
- screenshot "a", driver: :vips
+ screenshot "a"
assert_stored_screenshot("a.webp")
end
diff --git a/test/unit/driver_coverage_test.rb b/test/unit/driver_coverage_test.rb
deleted file mode 100644
index 3f4d9605..00000000
--- a/test/unit/driver_coverage_test.rb
+++ /dev/null
@@ -1,40 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "support/driver_coverage"
-
-class DriverCoverageTest < ActiveSupport::TestCase
- test "#banner lists detected drivers" do
- assert_equal "[capybara-screenshot-diff] drivers detected: chunky_png, vips",
- DriverCoverage.banner(%i[chunky_png vips])
- end
-
- test "#banner calls out unavailable drivers" do
- assert_equal "[capybara-screenshot-diff] drivers detected: chunky_png | unavailable: vips",
- DriverCoverage.banner(%i[chunky_png])
- end
-
- test "#missing_for_ci returns nothing when CI is unset" do
- assert_empty DriverCoverage.missing_for_ci(%i[chunky_png], ci: nil)
- end
-
- test "#missing_for_ci returns nothing when CI is blank" do
- assert_empty DriverCoverage.missing_for_ci(%i[chunky_png vips], ci: "")
- end
-
- test "#missing_for_ci returns nothing in CI when both drivers are available" do
- assert_empty DriverCoverage.missing_for_ci(%i[chunky_png vips], ci: "true")
- end
-
- test "#missing_for_ci flags vips missing in CI" do
- assert_equal [:vips], DriverCoverage.missing_for_ci(%i[chunky_png], ci: "true")
- end
-
- test "#missing_for_ci flags chunky_png missing in CI" do
- assert_equal [:chunky_png], DriverCoverage.missing_for_ci(%i[vips], ci: "true")
- end
-
- test "#missing_for_ci honors an explicit exclude override" do
- assert_empty DriverCoverage.missing_for_ci(%i[chunky_png], ci: "true", exclude: [:vips])
- end
-end
diff --git a/test/unit/drivers/chunky_png_driver_test.rb b/test/unit/drivers/chunky_png_driver_test.rb
deleted file mode 100644
index 30178337..00000000
--- a/test/unit/drivers/chunky_png_driver_test.rb
+++ /dev/null
@@ -1,161 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "snap_diff/comparison"
-require "snap_diff/drivers/chunky_png_driver"
-require "support/driver_contract_tests"
-
-# Nested in the canonical SnapDiff::Drivers namespace: reopening the old
-# Capybara::Screenshot::Diff::Drivers path would define a fresh Drivers
-# module there, shadowing the v2 step 6 lazy const_missing forwarder.
-module SnapDiff
- module Drivers
- class ChunkyPNGDriverTest < ActiveSupport::TestCase
- include DSLStub
- include DriverContractTests
-
- class QuickEqualTest < self
- test "#quick_equal? returns true when comparing identical images" do
- comp = make_comparison(:a, :a)
- assert comp.quick_equal?
- end
-
- test "#quick_equal? respects color_distance_limit setting when images are similar" do
- comp = make_comparison(:a, :b, color_distance_limit: 224)
- assert comp.quick_equal?
- end
- end
-
- class DifferentTest < self
- test "#different? returns false when comparing identical images" do
- comp = make_comparison(:a, :a)
- assert_not comp.different?
- end
-
- test "#different? respects tolerance setting when images differ slightly" do
- comp = make_comparison(:a, :b, tolerance: 2)
- assert_not comp.different?
- assert comp.quick_equal?
- end
-
- test "#different? identifies differences and generates annotated comparison images" do
- comp = make_comparison(:a, :c)
- assert comp.different?
- assert_includes comp.error_message, "[11,3,48,20]"
- assert File.exist?(comp.base_image_path)
- assert File.exist?(comp.reporter.annotated_base_image_path)
- assert File.exist?(comp.reporter.annotated_image_path)
-
- assert_same_images("a-and-c.diff.png", comp.reporter.annotated_base_image_path)
- assert_same_images("c-and-a.diff.png", comp.reporter.annotated_image_path)
- end
-
- test "#different? skips generating annotated images for identical images" do
- comp = make_comparison(:c, :c)
- assert_not comp.different?
-
- assert comp.reporter.annotated_base_image_path
- assert comp.reporter.annotated_image_path
-
- assert_not File.exist?(comp.reporter.annotated_base_image_path)
- assert_not File.exist?(comp.reporter.annotated_image_path)
- end
-
- test "#different? detects single-pixel width differences between images" do
- comp = make_comparison(:a, :d)
- assert comp.different?
- assert_includes comp.error_message, "[9,6,9,13]"
- end
-
- test "#different? respects shift_distance_limit when within allowed threshold" do
- comp = make_comparison(:a, :b, shift_distance_limit: 11)
- assert comp.quick_equal?
- assert_not comp.different?
- end
-
- test "#different? enforces shift_distance_limit when beyond allowed threshold" do
- comp = make_comparison(:a, :b, shift_distance_limit: 9)
- assert comp.different?
- assert_includes comp.error_message, "11"
- end
-
- test "#different? detects when images have different dimensions" do
- comp = make_comparison(:a, :a_cropped)
- assert comp.different?
- assert_includes comp.error_message, "Dimensions have changed: "
- assert_includes comp.error_message, "80x60"
- end
- end
-
- class ColorDistanceTest < self
- test "#different? respects color_distance_limit when within allowed threshold" do
- comp = make_comparison(:a, :b, color_distance_limit: 223)
- assert_not comp.different?
- end
-
- test "#different? enforces color_distance_limit when beyond allowed threshold" do
- comp = make_comparison(:a, :b, color_distance_limit: 222)
- assert comp.different?
- assert_includes comp.error_message, "222.7"
- end
-
- test "#max_color_distance returns expected value for images with minor differences" do
- comp = make_comparison(:a, :b)
- assert_not comp.quick_equal?
- comp.different?
- assert_includes comp.error_message, "85"
- end
-
- test "#max_color_distance returns expected value for images with moderate differences" do
- comp = make_comparison(:a, :c)
- comp.different?
- assert_includes comp.error_message, "187.4"
- end
-
- test "#max_color_distance returns expected value for images with significant differences" do
- comp = make_comparison(:a, :d)
- comp.different?
- assert_includes comp.error_message, "269.1"
- end
-
- test "#max_color_distance detects minimal color differences between images" do
- a_img = ChunkyPNG::Image.from_blob(File.binread("#{TEST_IMAGES_DIR}/a.png"))
- a_img[9, 6] += 0x010000
-
- comp = make_comparison(:a, :b)
- other_img_filename = comp.image_path
- a_img.save(other_img_filename)
-
- comp.different?
-
- assert_includes comp.error_message, "1"
- end
- end
-
- class HelpersTest < self
- test "#from_file successfully loads an image from the specified path" do
- driver = ChunkyPNGDriver.new
- assert driver.from_file("#{TEST_IMAGES_DIR}/a.png")
- end
-
- test "#supports? returns false for median filter" do
- driver = ChunkyPNGDriver.new
- refute driver.supports?(:filter_image_with_median)
- end
- end
-
- def make_comparison(old_img, new_img, options = {})
- snap = create_snapshot_for(old_img, new_img)
- SnapDiff::Comparison.new(snap.path, snap.base_path, **options)
- end
-
- def sample_region
- [0, 0, 0, 0]
- end
-
- def load_test_image(driver)
- driver.from_file("#{TEST_IMAGES_DIR}/a.png")
- end
- end
- end
-end
diff --git a/test/unit/drivers/utils_test.rb b/test/unit/drivers/utils_test.rb
deleted file mode 100644
index c2054253..00000000
--- a/test/unit/drivers/utils_test.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "snap_diff/utils"
-require "minitest/stub_const"
-
-class UtilsTest < ActiveSupport::TestCase
- test "#detect_available_drivers includes :vips when ruby-vips gem is available" do
- skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips)
- Object.stub :require, ->(gem) { gem == "vips" } do
- assert_includes SnapDiff::Utils.detect_available_drivers, :vips
- end
- end
-
- test "#detect_available_drivers excludes :vips when ruby-vips gem is not available" do
- Object.stub_remove_const(:Vips) do
- Object.stub :require, ->(gem) { gem != "vips" } do
- assert_not_includes SnapDiff::Utils.detect_available_drivers, :vips
- end
- end
- end
-
- test "#detect_available_drivers excludes :vips when system libvips is not installed" do
- Object.stub_remove_const(:Vips) do
- Object.stub :require, ->(gem) { gem == "vips" && raise(LoadError.new("Could not ... vips")) } do
- assert_not_includes SnapDiff::Utils.detect_available_drivers, :vips
- end
- end
- end
-
- test "#detect_available_drivers returns drivers in order of preference when multiple are available" do
- Object.stub_consts(Vips: Class.new, ChunkyPNG: Class.new) do
- Object.stub :require, true do
- assert_equal %i[vips chunky_png], SnapDiff::Utils.detect_available_drivers
- end
- end
- end
-
- test "#detect_available_drivers includes :chunky_png when the gem is available" do
- Object.stub :require, ->(gem) { gem == "chunky_png" } do
- assert_includes SnapDiff::Utils.detect_available_drivers, :chunky_png
- end
- end
-
- test "#detect_available_drivers excludes :chunky_png when the gem is not available" do
- Object.stub_remove_const(:ChunkyPNG) do
- Object.stub :require, ->(gem) { gem != "chunky_png" } do
- assert_not_includes SnapDiff::Utils.detect_available_drivers, :chunky_png
- end
- end
- end
-end
diff --git a/test/unit/drivers/vips_driver_test.rb b/test/unit/drivers/vips_driver_test.rb
index 1c70cfdb..d2051d96 100644
--- a/test/unit/drivers/vips_driver_test.rb
+++ b/test/unit/drivers/vips_driver_test.rb
@@ -3,11 +3,8 @@
require "test_helper"
require "support/driver_contract_tests"
-require "snap_diff/drivers/vips_driver" if defined?(Vips)
+require "snap_diff/drivers/vips_driver"
-# Nested in the canonical SnapDiff::Drivers namespace: reopening the old
-# Capybara::Screenshot::Diff::Drivers path would define a fresh Drivers
-# module there, shadowing the v2 step 6 lazy const_missing forwarder.
module SnapDiff
module Drivers
class VipsDriverTest < ActiveSupport::TestCase
@@ -15,7 +12,6 @@ class VipsDriverTest < ActiveSupport::TestCase
include DriverContractTests
setup do
- skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips)
@new_screenshot_result = Tempfile.new(%w[screenshot .png], Rails.root)
end
@@ -36,6 +32,10 @@ class VipsDriverTest < ActiveSupport::TestCase
# comparison then reads both -- a stale read compares against an image
# that is no longer on disk.
#
+ # It was LATENT while chunky_png existed: a Comparison built without an
+ # explicit driver defaulted to chunky_png, which re-read every time. 2.1
+ # makes vips the only backend, so this is now the only behaviour.
+ #
# The teardown above used to flush the whole vips cache
# (`Vips.cache_set_max(0); Vips.cache_set_max(1000)`) to paper over this;
# with the driver fixed, that workaround is gone.
@@ -194,7 +194,7 @@ class VipsDriverTest < ActiveSupport::TestCase
def make_comparison(old_img, new_img, options = {})
destination = Pathname.new(@new_screenshot_result.path)
- super(old_img, new_img, destination: destination, **options.merge(driver: :vips))
+ super(old_img, new_img, destination: destination, **options)
end
def sample_region
@@ -203,10 +203,6 @@ def sample_region
end
class VipsDriverClassMethodsTest < ActiveSupport::TestCase
- setup do
- skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips)
- end
-
test "VipsDriver.difference_region_by detects difference regions without color threshold" do
old_image = Vips::Image.new_from_file("#{TEST_IMAGES_DIR}/a.png")
new_image = Vips::Image.new_from_file("#{TEST_IMAGES_DIR}/b.png")
diff --git a/test/unit/drivers_test.rb b/test/unit/drivers_test.rb
deleted file mode 100644
index 43271317..00000000
--- a/test/unit/drivers_test.rb
+++ /dev/null
@@ -1,91 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "open3"
-require "minitest/stub_const"
-
-# SnapDiff::Drivers is the canonical driver registry (docs/snapdiff.md).
-# Until 3.0 readiness work, `.available` read the value out of
-# SnapDiff::Drivers::AVAILABLE_DRIVERS, which is defined by
-# config_legacy.rb -- so a documented canonical API only worked when the v1
-# tree happened to be loaded.
-class DriversTest < ActiveSupport::TestCase
- # The regression: `require "snap_diff/drivers"` alone used to raise
- # `NameError: uninitialized constant Capybara::Screenshot::Diff` here.
- # test_helper preloads the whole gem, so only a fresh process can catch it.
- test ".available answers after requiring snap_diff/drivers and nothing else" do
- script = <<~RUBY
- require "snap_diff/drivers"
- drivers = SnapDiff::Drivers.available
- abort("not an Array: \#{drivers.inspect}") unless drivers.is_a?(Array)
- # ...and it answered on its own, without config_legacy.rb being loaded.
- # (Constants alone would not prove it: bundler/setup evaluates the
- # gemspec, which loads the legacy version.rb and so defines
- # Capybara::Screenshot::Diff in any subprocess.)
- legacy = $LOADED_FEATURES.grep(/config_legacy\\.rb\\z/)
- abort("config_legacy got loaded: \#{legacy.join(", ")}") unless legacy.empty?
- RUBY
-
- out, status = Open3.capture2e(RbConfig.ruby, "-Ilib", "-e", script)
-
- assert status.success?, "expected standalone snap_diff/drivers to answer .available, got:\n#{out}"
- end
-
- test ".available reads the constant live, so it stays stubbable" do
- SnapDiff::Drivers.stub_const(:AVAILABLE_DRIVERS, []) do
- assert_empty SnapDiff::Drivers.available
- end
-
- assert_equal SnapDiff::Drivers::AVAILABLE_DRIVERS, SnapDiff::Drivers.available
- end
-
- # Ported from namespace_forwarding_test (test/legacy/), which was the only
- # place pinning this: it asserted the v1 LOADED_DRIVERS constant is the
- # same object as this hash and that a registration through it shows up
- # here. The v1 half dies in 2.1; the canonical half -- .loaded is ONE
- # memoized hash, mutated in place, so a driver registered into it stays
- # registered (Utils.find_driver_class_for caches through it) -- must not.
- test ".loaded is a single hash mutated in place, so registrations stick" do
- assert_same SnapDiff::Drivers.loaded, SnapDiff::Drivers.loaded
-
- SnapDiff::Drivers.loaded[:registry_probe] = :probe_driver
-
- assert_equal :probe_driver, SnapDiff::Drivers.loaded[:registry_probe]
- ensure
- SnapDiff::Drivers.loaded.delete(:registry_probe)
- end
-
- test "detection is reachable under both the canonical and the documented Utils name" do
- assert_equal SnapDiff::Drivers.detect_available, SnapDiff::Utils.detect_available_drivers
- end
-
- # Naming a driver class must work without a prior require (the leaves are
- # autoloaded), but ONLY for drivers this process can actually load.
- # Neither driver gem is a runtime dependency, and the documented v1
- # pattern is `driver = :vips if defined?(...Drivers::VipsDriver)`: an
- # unconditional autoload makes that truthy on a box without ruby-vips and
- # the branch then dies on const_get. v1.12.0 loaded vips_driver.rb only
- # from find_driver_class_for, so `defined?` was nil there.
- test "an unavailable driver leaf stays undefined rather than autoloading into a crash" do
- script = <<~RUBY
- module Kernel
- alias_method :__real_require, :require
- def require(name)
- raise LoadError, "cannot load such file -- vips" if name == "vips"
- __real_require(name)
- end
- end
- require "snap_diff/drivers"
-
- abort("vips reported available") if SnapDiff::Drivers.available.include?(:vips)
- abort("VipsDriver is const_defined? without ruby-vips") if
- SnapDiff::Drivers.const_defined?(:VipsDriver)
- abort("chunky_png leaf should still autoload") unless
- SnapDiff::Drivers::ChunkyPNGDriver.is_a?(Class)
- RUBY
-
- out, status = Open3.capture2e(RbConfig.ruby, "-Ilib", "-e", script)
-
- assert status.success?, out
- end
-end
diff --git a/test/unit/dsl_test.rb b/test/unit/dsl_test.rb
index 39015f35..cdc49110 100644
--- a/test/unit/dsl_test.rb
+++ b/test/unit/dsl_test.rb
@@ -31,11 +31,16 @@ def after_teardown
end
end
+ # The reported figures are libvips': area_size and region come out as floats
+ # and there is no max_color_distance. They used to be chunky_png's (629 /
+ # [11,3,48,20] / max_color_distance 187.4) because a Comparison built without
+ # an explicit `driver:` fell back to chunky_png -- 2.1 deleted that driver
+ # and the selection that reached it, so this is the deletion showing through,
+ # not a drift in the reporter.
test "#assert_image_not_changed generates correct error message for image mismatch" do
message = assert_image_not_changed(["my_test.rb:42"], "name", make_comparison(:a, :c, destination: "screenshot.png"))
- value = (RUBY_VERSION >= "2.4") ? 187.4 : 188
assert_equal <<~MSG.chomp, message
- Screenshot does not match for 'name': ({"area_size":629,"region":[11,3,48,20],"max_color_distance":#{value}})
+ Screenshot does not match for 'name': ({"area_size":684.0,"region":[11.0,3.0,49.0,21.0]})
#{SnapDiff.config.root}/doc/screenshots/screenshot.png
#{SnapDiff.config.root}/doc/screenshots/screenshot.base.diff.png
#{SnapDiff.config.root}/doc/screenshots/screenshot.diff.png
@@ -44,26 +49,16 @@ def after_teardown
MSG
end
- test "#assert_image_not_changed includes shift distance in error message when specified" do
- message = assert_image_not_changed(
- ["my_test.rb:42"],
- "name",
- make_comparison(:a, :c, destination: "screenshot.png", shift_distance_limit: 1, driver: :chunky_png)
- )
- value = (RUBY_VERSION >= "2.4") ? 5.0 : 5
- assert_equal <<~MSG.chomp, message
- Screenshot does not match for 'name': ({"area_size":629,"region":[11,3,48,20],"max_color_distance":#{value},"max_shift_distance":15})
- #{SnapDiff.config.root}/doc/screenshots/screenshot.png
- #{SnapDiff.config.root}/doc/screenshots/screenshot.base.diff.png
- #{SnapDiff.config.root}/doc/screenshots/screenshot.diff.png
- #{SnapDiff.config.root}/doc/screenshots/screenshot.heatmap.diff.png
- my_test.rb:42
- MSG
- end
+ # Two tests are deleted rather than repointed:
+ #
+ # - "includes shift distance in error message": `shift_distance_limit` is
+ # implemented only by chunky_png and dies with it. libvips has no
+ # shift-distance comparison, so there is no equivalent message.
+ # - "supports driver options for image comparison": there are no driver
+ # options left to support.
- test "#screenshot supports driver options for image comparison" do
- skip "vips is disabled" unless defined?(Vips)
- assert_not screenshot("a", driver: :vips)
+ test "#screenshot compares against the baseline and reports no difference" do
+ assert_not screenshot("a")
end
def assert_no_screenshot_jobs_scheduled
diff --git a/test/unit/image_compare_test.rb b/test/unit/image_compare_test.rb
index 89c2bc07..02196aef 100644
--- a/test/unit/image_compare_test.rb
+++ b/test/unit/image_compare_test.rb
@@ -1,36 +1,19 @@
# frozen_string_literal: true
require "test_helper"
-require "minitest/stub_const"
-require "snap_diff/drivers/chunky_png_driver"
-if defined?(Vips)
- require "snap_diff/drivers/vips_driver"
-elsif ENV["SCREENSHOT_DRIVER"] == "vips"
- raise 'Required `ruby-vips` gem or `vips` library is missing. Ensure "ruby-vips" gem and "vips" library is installed.'
-end
+# No `if defined?(Vips)` guard and no driver-selection tests: 2.1 removed the
+# driver abstraction, so `ruby-vips` is a gemspec runtime dependency and
+# SnapDiff::Drivers::VipsDriver is the only backend there is.
class ImageCompareTest < ActiveSupport::TestCase
include DSLStub
- test "#initialize creates instance with chunky_png driver by default" do
- comparison = make_comparison(:b)
- assert_kind_of SnapDiff::Drivers::ChunkyPNGDriver, comparison.driver
- end
-
- test "#initialize creates instance with explicit chunky_png driver" do
- comparison = make_comparison(:b, driver: :chunky_png)
- assert_kind_of SnapDiff::Drivers::ChunkyPNGDriver, comparison.driver
- end
-
- test "#initialize creates instance with vips driver when specified" do
- skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips)
- comparison = make_comparison(:b, driver: :vips)
- assert_kind_of SnapDiff::Drivers::VipsDriver, comparison.driver
+ test "#initialize always builds the vips driver" do
+ assert_kind_of SnapDiff::Drivers::VipsDriver, make_comparison(:b).driver
end
- test "#different? with vips driver generates annotated diff images" do
- skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips)
- comparison = make_comparison(:a, :b, driver: :vips)
+ test "#different? generates annotated diff images" do
+ comparison = make_comparison(:a, :b)
assert comparison.different?
@@ -38,26 +21,18 @@ class ImageCompareTest < ActiveSupport::TestCase
assert_same_images("b-and-a.diff.png", comparison.reporter.annotated_image_path)
end
- test "#different? handles very long input filenames with vips driver" do
- skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips)
+ test "#different? handles very long input filenames" do
filename = %w[this-0000000000000000000000000000000000000000000000000-path/is/extremely/
long/and/if/the/directories/are/flattened/in/
the_temporary_they_will_cause_the_filename_to_exceed_
the_limit_on_most_unix_systems_which_nobody_wants.png].join
- comparison = make_comparison(:a, :b, destination: (Rails.root / filename), driver: :vips)
+ comparison = make_comparison(:a, :b, destination: (Rails.root / filename))
assert comparison.different?
end
- test "#initialize with vips driver respects tolerance option" do
- skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips)
- comp = make_comparison(:a, :b, driver: :vips, tolerance: 0.02)
- assert comp.quick_equal?
- assert_not comp.different?
- end
-
- test "#initialize with chunky_png driver respects tolerance option" do
- comp = make_comparison(:a, :b, driver: :chunky_png, tolerance: 0.02)
+ test "#initialize respects the tolerance option" do
+ comp = make_comparison(:a, :b, tolerance: 0.02)
assert comp.quick_equal?
assert_not comp.different?
assert_equal 0.02, comp.driver_options[:tolerance]
@@ -68,26 +43,6 @@ class ImageCompareTest < ActiveSupport::TestCase
assert comp.quick_equal?
assert_not comp.different?
end
-
- test "#initialize with :auto driver selects vips when available" do
- skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips)
- comparison = make_comparison(:b, driver: :auto)
- assert_kind_of SnapDiff::Drivers::VipsDriver, comparison.driver
- end
-
- test "#initialize with :auto driver raises error when no drivers available" do
- # Canonical stubbing point since the detected-drivers list moved to
- # SnapDiff::Drivers (2.1 readiness). The legacy
- # Capybara::Screenshot::Diff::AVAILABLE_DRIVERS is now an eager
- # same-object ALIAS of this constant, so stubbing the old name only
- # rebinds the alias and no longer reaches the core.
- SnapDiff::Drivers.stub_const(:AVAILABLE_DRIVERS, []) do
- assert_raise(RuntimeError) do
- comparison = make_comparison(:b, driver: :auto)
- assert comparison.quick_equal?
- end
- end
- end
end
# Guards the regression killed twice during ADR-004 review (migration-plan PR 5): skip_area
@@ -99,15 +54,13 @@ class ImageCompareTest < ActiveSupport::TestCase
#
# `make_comparison(:a, :c)` stands in for that scenario: `:a` plays the already-on-disk
# baseline (as if checked out from VCS), `:c` plays the freshly captured screenshot. The two
-# fixtures are known to differ only within [11,3,48,20] (see ChunkyPNGDriverTest above).
+# fixtures are known to differ only within [11,3,48,20].
class SkipAreaMasksVcsBaselineTest < ActiveSupport::TestCase
include DSLStub
test "#different? masks the VCS-checked-out baseline, not just the new screenshot" do
- skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips)
-
full_image_region = Region.from_edge_coordinates(0, 0, 80, 80)
- comparison = make_comparison(:a, :c, destination: "skip_area_vcs_baseline", driver: :vips, skip_area: [full_image_region])
+ comparison = make_comparison(:a, :c, destination: "skip_area_vcs_baseline", skip_area: [full_image_region])
refute_predicate comparison, :different?
end
@@ -116,24 +69,17 @@ class SkipAreaMasksVcsBaselineTest < ActiveSupport::TestCase
class IntegrationRegressionTest < ActiveSupport::TestCase
include DSLStub
- AVAILABLE_DRIVERS = [{}, {driver: :chunky_png}]
-
- test "identical images are quick_equal and not different across all drivers" do
+ # Was a two-element driver matrix ({} and {driver: :chunky_png}); with one
+ # backend the outer loop had one iteration, so it is gone rather than left
+ # as a loop over a single element.
+ test "identical images are quick_equal and not different" do
images = all_fixtures_images_names
- AVAILABLE_DRIVERS.each do |driver|
- Dir.chdir File.expand_path("../fixtures/images", __dir__) do
- images.each do |old_img|
- new_img = old_img
- comparison = make_comparison(old_img, new_img, **driver)
- assert(
- comparison.quick_equal?,
- "compare #{old_img} with #{new_img} with #{driver} driver should be quick_equal"
- )
- assert_not(
- comparison.different?,
- "compare #{old_img} with #{new_img} with #{driver} driver should not be different"
- )
- end
+ Dir.chdir File.expand_path("../fixtures/images", __dir__) do
+ images.each do |old_img|
+ new_img = old_img
+ comparison = make_comparison(old_img, new_img)
+ assert(comparison.quick_equal?, "compare #{old_img} with #{new_img} should be quick_equal")
+ assert_not(comparison.different?, "compare #{old_img} with #{new_img} should not be different")
end
end
end
@@ -141,20 +87,18 @@ class IntegrationRegressionTest < ActiveSupport::TestCase
test "different images are not quick_equal and are marked as different" do
images = all_fixtures_images_names
- AVAILABLE_DRIVERS.each do |driver|
- images.each do |image|
- other_images = images - [image]
- other_images.each do |different_image|
- comparison = make_comparison(image, different_image, **driver)
- assert_not(
- comparison.quick_equal?,
- "compare #{image.inspect} with #{different_image.inspect} using #{driver} driver should not be quick_equal"
- )
- assert(
- comparison.different?,
- "compare #{image.inspect} with #{different_image.inspect} using #{driver} driver should be different"
- )
- end
+ images.each do |image|
+ other_images = images - [image]
+ other_images.each do |different_image|
+ comparison = make_comparison(image, different_image)
+ assert_not(
+ comparison.quick_equal?,
+ "compare #{image.inspect} with #{different_image.inspect} should not be quick_equal"
+ )
+ assert(
+ comparison.different?,
+ "compare #{image.inspect} with #{different_image.inspect} should be different"
+ )
end
end
end
diff --git a/test/unit/image_preprocessor_test.rb b/test/unit/image_preprocessor_test.rb
index 21e67d0a..1629ea2f 100644
--- a/test/unit/image_preprocessor_test.rb
+++ b/test/unit/image_preprocessor_test.rb
@@ -43,9 +43,7 @@ def setup
assert_equal :new_image, second_call[:image]
end
- test "#process_comparison applies median filter when VipsDriver is available and median_filter_window_size is specified" do
- skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips)
-
+ test "#process_comparison applies the median filter when median_filter_window_size is specified" do
@driver = create_test_driver(is_vips: true)
window_size = 3
options = {median_filter_window_size: window_size}
@@ -66,25 +64,8 @@ def setup
assert_equal :new_image, second_call[:image]
end
- test "process_comparison warns and skips median filter when VipsDriver is not available" do
- window_size = 3
- options = {
- median_filter_window_size: window_size,
- image_path: "some/path.png"
- }
-
- expected_warning = /Median filter has been skipped for.*because it is not supported/
-
- comparison = SnapDiff::Comparison::Images.new(:new_image, :base_image, {}, @driver)
-
- warning_output = capture_io do
- preprocessor = SnapDiff::ImagePreprocessor.new(@driver, options)
- result = preprocessor.process_comparison(comparison)
-
- assert_equal comparison, result
- assert_empty @driver.filter_calls
- end
-
- assert_match expected_warning, warning_output.join
- end
+ # The "warns and skips the median filter when the driver does not support it"
+ # test is gone with the capability probe it exercised: chunky_png was the
+ # driver that lacked #filter_image_with_median, and 2.1 removed it. With
+ # libvips the only backend the fallback branch was unreachable.
end
diff --git a/test/unit/legacy_deletion_test.rb b/test/unit/legacy_deletion_test.rb
deleted file mode 100644
index 0a79bd5e..00000000
--- a/test/unit/legacy_deletion_test.rb
+++ /dev/null
@@ -1,201 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "open3"
-require "tmpdir"
-require "fileutils"
-require "unit/support_load_probe_test" # single source of truth for the canonical entry-point tables
-
-# THE 2.1 DELETION, ACTUALLY RUN.
-#
-# legacy_tree_is_alias_only_test.rb and core_tree_has_no_legacy_deps_test.rb
-# are STATIC proxies for one claim: `git rm` the v1 surface and the gem still
-# loads. This test stops proxying. It copies lib/ to a tmpdir, performs the
-# deletion, applies the edits the deletion needs, and requires every canonical
-# entry point in a fresh subprocess.
-#
-# THE GATE LINE (see GATE_SCRIPT) is why this is evidence rather than
-# decoration. An earlier lane's "green" run turned out to have measured the
-# INTACT tree: BUNDLE_GEMFILE pointed at the gemspec, which unshifts the real
-# lib/ onto $LOAD_PATH ahead of any -I. A run that cannot tell the deleted
-# tree from the intact one proves nothing, so before asserting anything about
-# the surface every probe HARD-ASSERTS that the deletion is in effect -- the
-# deleted names are gone AND every snap_diff file that loaded came from the
-# tmpdir. The subprocess is isolated from bundler as well (see #probe), but
-# that isolation is precisely the thing that silently stopped working last
-# time -- the gate line is what notices when it does.
-class LegacyDeletionTest < ActiveSupport::TestCase
- PROJECT_ROOT = Pathname.new(File.expand_path("../..", __dir__))
-
- # The 2.1 `git rm`, verbatim from the Rakefile's header comment (minus
- # test/legacy, which this test does not load).
- DELETED = %w[
- capybara
- capybara_screenshot_diff
- capybara-screenshot-diff.rb
- capybara_screenshot_diff.rb
- snap_diff/legacy_shims.rb
- snap_diff/deprecation.rb
- ].freeze
-
- # The edits the deletion needs, as [file, exact line to remove or replace,
- # replacement or nil]. Exact-match on purpose: if one of these lines is
- # reworded, the edit must go red here rather than silently not applying and
- # leaving the probe to fail somewhere confusing.
- EDITS = [
- # The one line in the canonical entry point that 2.1 drops.
- ["snap_diff.rb", %(require "snap_diff/legacy_shims"), nil],
- # The new gem name's Bundler entry point is KEPT, repointed off the v1
- # umbrella. It matches neither gate's file glob, so this is the only
- # thing that checks its post-2.1 shape at all.
- ["snap_diff-capybara.rb",
- %(require "capybara_screenshot_diff/minitest"),
- %(require "snap_diff/integrations/minitest")]
- ].freeze
-
- ENTRY_POINTS = SupportLoadProbeTest::CANONICAL_ENTRY_POINTS
-
- # Runs FIRST in every probe, before a single surface assertion. Proves the
- # process is looking at the deleted tree and nothing else.
- GATE_SCRIPT = <<~'RUBY'
- tree = ENV.fetch("DELETED_TREE")
- gate = []
-
- # Defined in legacy_shims.rb; its presence means the deletion did not take.
- gate << "SnapDiff.start is still defined" if SnapDiff.respond_to?(:start)
- gate << "CapybaraScreenshotDiff is still defined" if defined?(CapybaraScreenshotDiff)
-
- deleted = $LOADED_FEATURES.grep(%r{/snap_diff/(legacy_shims|deprecation)\.rb\z})
- gate << "deleted files loaded: #{deleted.join(", ")}" unless deleted.empty?
-
- v1 = $LOADED_FEATURES.grep(%r{/lib/capybara(-screenshot-diff|_screenshot_diff|/screenshot)})
- gate << "v1 tree loaded: #{v1.join(", ")}" unless v1.empty?
-
- # The BUNDLE_GEMFILE trap: files resolving from the INTACT lib/ while the
- # tmpdir sits unused on the load path.
- #
- # Anchored on the library path, NOT a bare /snap_diff/ substring: CI checks
- # this repo out at .../snap_diff-capybara/, so a bare match flags every gem
- # under vendor/bundle (nokogiri and friends) and the gate fails everywhere
- # except a dev machine whose directory happens to be named otherwise.
- strays = $LOADED_FEATURES.grep(%r{/lib/snap_diff(/|\.rb\z)}).reject { |f| f.start_with?(tree) }
- gate << "loaded from outside the deleted tree: #{strays.join(", ")}" unless strays.empty?
-
- unless gate.empty?
- abort("GATE: this process is NOT running the deleted tree, so nothing below is evidence:\n- " + gate.join("\n- "))
- end
- RUBY
-
- test "every canonical entry point loads and keeps its surface after the 2.1 deletion" do
- in_deleted_tree do |tree|
- failures = ENTRY_POINTS.filter_map do |entry, methods|
- probe(tree, <<~RUBY)
- require #{entry.inspect}
- #{GATE_SCRIPT}
- missing = #{methods.inspect}.reject { |m| SnapDiff.respond_to?(m) }
- missing << "VERSION" unless defined?(SnapDiff::VERSION)
- abort("missing: \#{missing.join(", ")}") unless missing.empty?
- RUBY
- end
-
- assert_empty failures, <<~MSG
- `git rm` of the v1 surface breaks canonical entry point(s) -- 2.1 is a
- refactor, not a deletion, until these load:
-
- #{failures.join("\n")}
- MSG
- end
- end
-
- test "every canonical entry point defines its advertised constants after the 2.1 deletion" do
- in_deleted_tree do |tree|
- failures = SupportLoadProbeTest::CANONICAL_ADVERTISED_CONSTANTS.filter_map do |entry, constants|
- probe(tree, <<~RUBY)
- require #{entry.inspect}
- #{GATE_SCRIPT}
- missing = #{constants.inspect}.reject { |c| Object.const_defined?(c) }
- abort("missing: \#{missing.join(", ")}") unless missing.empty?
- RUBY
- end
-
- assert_empty failures, <<~MSG
- Entry point(s) lose their advertised constants once the v1 surface is deleted:
-
- #{failures.join("\n")}
- MSG
- end
- end
-
- # The gate line has to be able to FAIL, or it is a comment with an `if`
- # around it. Same probe, run against an UNTOUCHED copy of lib/: every
- # surface assertion would pass there, so only the gate can reject it.
- test "the gate line rejects an intact tree" do
- Dir.mktmpdir("snapdiff_intact") do |dir|
- tree = copy_lib_to(dir)
-
- failure = probe(tree, <<~RUBY)
- require "snap_diff"
- #{GATE_SCRIPT}
- RUBY
-
- assert failure, "the gate line passed on an INTACT tree -- it cannot distinguish the deletion"
- assert_includes failure, "SnapDiff.start is still defined"
- end
- end
-
- private
-
- # $LOADED_FEATURES holds resolved real paths, and Dir.mktmpdir hands back
- # the symlinked /var form on macOS -- so the gate's "outside the tree"
- # check compared /private/var/... against /var/... and rejected the very
- # tree it had just built. Resolve once, here.
- def copy_lib_to(dir)
- FileUtils.cp_r(PROJECT_ROOT.join("lib").to_s, File.join(dir, "lib"))
- File.realpath(File.join(dir, "lib"))
- end
-
- # Yields the path to a lib/ with the 2.1 deletion applied.
- def in_deleted_tree
- Dir.mktmpdir("snapdiff_deleted") do |dir|
- tree = copy_lib_to(dir)
-
- DELETED.each do |path|
- target = File.join(tree, path)
- assert File.exist?(target), "2.1 deletion set names #{path}, which does not exist"
- FileUtils.rm_rf(target)
- end
-
- EDITS.each do |file, line, replacement|
- target = Pathname.new(File.join(tree, file))
- source = target.read
-
- assert_includes source, line, "2.1 edit for #{file} no longer matches the file"
- target.write(source.sub(line + "\n", replacement ? replacement + "\n" : ""))
- end
-
- yield tree
- end
- end
-
- # A fresh process with ONLY +tree+ on the load path.
- #
- # `chdir: tree` is the load-bearing half, and NOT a detail. Scrubbing
- # RUBYOPT/BUNDLE_GEMFILE is not sufficient on its own: with the cwd still
- # inside the project, RubyGems auto-discovers gems.rb, puts
- # `-rbundler/setup` BACK into RUBYOPT, and the gemspec unshifts the real
- # lib/ ahead of the -I dir -- measured, this exact scrub with cwd at the
- # project root loads 24 files from the intact tree. Running from the
- # tmpdir means there is no gems.rb to find. Both defenses are here because
- # the gate line inside the script is the only one that says so out loud
- # when they stop working.
- def probe(tree, script)
- preamble = <<~RUBY
- $LOAD_PATH.unshift(#{tree.inspect})
- #{SupportLoadProbeTest::CUCUMBER_RUNTIME_STUB}
- RUBY
- env = {"DELETED_TREE" => tree, "RUBYOPT" => nil, "BUNDLE_GEMFILE" => nil, "RUBYLIB" => nil}
- out, status = Open3.capture2e(env, RbConfig.ruby, "-e", preamble + script, chdir: tree)
-
- "#{script.lines.first.strip} -> #{out}" unless status.success?
- end
-end
diff --git a/test/unit/removed_in_2_1_deprecation_test.rb b/test/unit/removed_in_2_1_deprecation_test.rb
deleted file mode 100644
index f10cf3c7..00000000
--- a/test/unit/removed_in_2_1_deprecation_test.rb
+++ /dev/null
@@ -1,249 +0,0 @@
-# frozen_string_literal: true
-
-require "test_helper"
-require "open3"
-
-# THE 2.1 REMOVALS, ANNOUNCED IN 2.0.
-#
-# 2.0 is the transitional release: the contract is published before it is
-# enforced, so everything 2.1 deletes has to warn HERE, naming 2.1, while it
-# still works. The legacy-namespace half of that promise is covered by
-# test/legacy/; this file covers the driver half, which 2.1 removes whole:
-# the chunky_png driver, `shift_distance_limit` (chunky-only, it dies with
-# it), and the driver abstraction itself (`SnapDiff::Driver`,
-# `SnapDiff::Drivers.loaded` / `.available`, `driver: :auto`) -- libvips
-# becomes the only backend.
-#
-# Every example runs in a SUBPROCESS. "Once per process" is the contract, and
-# this suite is a single long-lived process that selects chunky_png in
-# hundreds of tests (test_helper suppresses these warnings for exactly that
-# reason) -- an in-process assertion could measure neither.
-class RemovedIn21DeprecationTest < ActiveSupport::TestCase
- PROJECT_ROOT = File.expand_path("../..", __dir__)
- IMAGE_A = File.join(PROJECT_ROOT, "test/fixtures/images/a.png")
- IMAGE_B = File.join(PROJECT_ROOT, "test/fixtures/images/b.png")
-
- # Blocks `require "vips"` so detection reports chunky_png only -- the
- # `driver: :auto` fallback a user without libvips is silently on today.
- # (Same technique as drivers_test's unavailable-leaf probe.)
- NO_VIPS = <<~RUBY
- module Kernel
- alias_method :__real_require, :require
- def require(name)
- raise LoadError, "cannot load such file -- vips" if name == "vips"
- __real_require(name)
- end
- end
- RUBY
-
- # --- the chunky_png driver ------------------------------------------
-
- test "selecting chunky_png per comparison warns once, naming 2.1" do
- lines = probe(<<~RUBY)
- require "snap_diff"
- 3.times { #{compare(driver: :chunky_png)} }
- RUBY
-
- assert_equal 1, lines.size, lines.join
- assert_match(/chunky_png/, lines.first)
- assert_match(/REMOVED in 2\.1/, lines.first)
- assert_match(/vips/, lines.first)
- end
-
- test "selecting chunky_png through SnapDiff.config.driver warns once" do
- lines = probe(<<~RUBY)
- require "snap_diff"
- SnapDiff.config.driver = :chunky_png
- 3.times { #{compare} }
- RUBY
-
- assert_equal 1, lines.size, lines.join
- assert_match(/chunky_png/, lines.first)
- end
-
- # THE CASE THAT MATTERS MOST: these users never asked for chunky_png and
- # have no idea they are on it, so the warning has to say why they are.
- test "the :auto fallback to chunky_png warns and says libvips is missing" do
- lines = probe(<<~RUBY)
- #{NO_VIPS}
- require "snap_diff"
- 3.times { #{compare} }
- RUBY
-
- assert_equal 1, lines.size, lines.join
- assert_match(/auto/, lines.first)
- assert_match(/libvips/, lines.first)
- assert_match(/REMOVED in 2\.1/, lines.first)
- end
-
- # --- shift_distance_limit (chunky-only, dies with it) ----------------
-
- test "setting shift_distance_limit on the config warns once" do
- lines = probe(<<~RUBY)
- require "snap_diff"
- 3.times { SnapDiff.config.shift_distance_limit = 5 }
- RUBY
-
- assert_equal 1, lines.size, lines.join
- assert_match(/shift_distance_limit/, lines.first)
- assert_match(/REMOVED in 2\.1/, lines.first)
- end
-
- # Counts the shift lines rather than every line: the driver is left to
- # `:auto` so this runs on a box with or without libvips, and a box without
- # it legitimately gets the chunky_png fallback warning as well.
- test "passing shift_distance_limit per comparison warns once" do
- lines = probe(<<~RUBY)
- require "snap_diff"
- 3.times { #{compare(shift_distance_limit: 5)} }
- RUBY
-
- shift = lines.grep(/shift_distance_limit/)
- assert_equal 1, shift.size, lines.join
- assert_match(/REMOVED in 2\.1/, shift.first)
- end
-
- # --- the driver abstraction ------------------------------------------
-
- test "reading the custom-driver registry warns once" do
- lines = probe(<<~RUBY)
- require "snap_diff/drivers"
- 3.times { SnapDiff::Drivers.loaded[:mine] = Class.new }
- RUBY
-
- assert_equal 1, lines.size, lines.join
- assert_match(/SnapDiff::Drivers\.loaded/, lines.first)
- assert_match(/REMOVED in 2\.1/, lines.first)
- end
-
- test "reading the detected-driver list warns once" do
- lines = probe(<<~RUBY)
- require "snap_diff/drivers"
- 3.times { SnapDiff::Drivers.available }
- RUBY
-
- assert_equal 1, lines.size, lines.join
- assert_match(/SnapDiff::Drivers\.available/, lines.first)
- end
-
- test "a custom driver including the Driver mixin warns once" do
- lines = probe(<<~RUBY)
- require "snap_diff/driver"
- class MyDriver
- include SnapDiff::Driver
- end
- class MyOtherDriver
- include SnapDiff::Driver
- end
- RUBY
-
- assert_equal 1, lines.size, lines.join
- assert_match(/include SnapDiff::Driver/, lines.first)
- assert_match(/REMOVED in 2\.1/, lines.first)
- end
-
- # --- what must stay silent -------------------------------------------
-
- # The mutation that matters for everyone who is NOT affected: a plain vips
- # setup, comparing images, must not gain a single line of stderr -- and the
- # gem's own drivers include the mixin themselves, so an unscoped `included`
- # hook would fire here.
- test "a plain vips setup with no chunky or shift usage stays silent" do
- skip "libvips not available on this box" unless SnapDiff::Drivers::AVAILABLE_DRIVERS.include?(:vips)
-
- out = probe_stderr(<<~RUBY)
- require "snap_diff"
- SnapDiff.config.driver = :vips
- 3.times { #{compare} }
- SnapDiff::Drivers::VipsDriver
- RUBY
-
- assert_equal "", out, "a vips-only setup must not warn"
- end
-
- # The other half of "the gem must not warn at itself": on a box with NO
- # libvips, every internal load path runs through chunky_png. Loading the
- # gem still has to be silent -- the warning belongs to the first
- # comparison the user asks for, not to `require`.
- test "loading the gem selects nothing and stays silent even without libvips" do
- out = probe_stderr(<<~RUBY)
- #{NO_VIPS}
- require "snap_diff"
- SnapDiff.config
- RUBY
-
- assert_equal "", out, "requiring the gem must not select a driver"
- end
-
- test "every warning fires exactly once per process, however many surfaces are touched" do
- lines = probe(<<~RUBY)
- require "snap_diff"
- 3.times do
- SnapDiff.config.shift_distance_limit = 5
- SnapDiff.config.driver = :chunky_png
- #{compare}
- SnapDiff::Drivers.loaded
- SnapDiff::Drivers.available
- Class.new { include SnapDiff::Driver }
- end
- RUBY
-
- assert_equal 5, lines.size, lines.join
- assert_equal 5, lines.uniq.size, "duplicate warning text: #{lines.join}"
- end
-
- # --- silencing --------------------------------------------------------
-
- test "silenced by the SnapDiff.silence_deprecations accessor" do
- out = probe_stderr(<<~RUBY)
- require "snap_diff"
- SnapDiff.silence_deprecations = true
- SnapDiff.config.shift_distance_limit = 5
- #{compare(driver: :chunky_png)}
- SnapDiff::Drivers.loaded
- SnapDiff::Drivers.available
- Class.new { include SnapDiff::Driver }
- RUBY
-
- assert_equal "", out
- end
-
- test "silenced by SNAP_DIFF_SILENCE_DEPRECATIONS" do
- out = probe_stderr(<<~RUBY, "SNAP_DIFF_SILENCE_DEPRECATIONS" => "1")
- require "snap_diff"
- SnapDiff.config.shift_distance_limit = 5
- #{compare(driver: :chunky_png)}
- SnapDiff::Drivers.loaded
- SnapDiff::Drivers.available
- Class.new { include SnapDiff::Driver }
- RUBY
-
- assert_equal "", out
- end
-
- private
-
- # `SnapDiff.compare(base, new, **options)` against two real fixtures --
- # the documented entry point, so the probes exercise driver selection the
- # way an adopter reaches it rather than by poking at internals.
- def compare(**options)
- args = [IMAGE_A.inspect, IMAGE_B.inspect]
- options.each { |key, value| args << "#{key}: #{value.inspect}" }
- "SnapDiff.compare(#{args.join(", ")})"
- end
-
- def probe(script, env = {})
- probe_stderr(script, env).lines.reject { |line| line.strip.empty? }
- end
-
- # Runs +script+ in a fresh process with only lib/ on the load path and
- # returns this gem's warnings from its stderr (other gems' warnings are
- # none of this test's business).
- def probe_stderr(script, env = {})
- _out, err, status = Open3.capture3(
- env, RbConfig.ruby, "-Ilib", "-e", script, chdir: PROJECT_ROOT
- )
- assert_predicate status, :success?, err
- err.lines.grep(/\[snap_diff/).join
- end
-end
diff --git a/test/unit/removed_surface_test.rb b/test/unit/removed_surface_test.rb
new file mode 100644
index 00000000..1c9f2d8c
--- /dev/null
+++ b/test/unit/removed_surface_test.rb
@@ -0,0 +1,322 @@
+# frozen_string_literal: true
+
+require "test_helper"
+require "open3"
+
+# THE 2.1 DELETION, ASSERTED AS ABSENCE.
+#
+# Its predecessor (legacy_deletion_test.rb) SIMULATED the deletion -- copy
+# lib/, `rm` the trees, probe entry points -- because the trees were still
+# there. They are not, so the simulation harness went with them: what every
+# canonical entry point still LOADS is already checked against the real lib/
+# on every run by support_load_probe_test.rb.
+#
+# What nothing else checks is the negative, which is exactly the claim a bad
+# rebase or an over-eager revert breaks quietly:
+#
+# 1. No removed path is back under lib/. Everything there is packaged
+# (see the gemspec's `spec.files` glob), so a restored file SHIPS.
+# 2. A fresh process that loads the gem defines none of the removed names.
+#
+# THE GATE LINE. A probe that cannot tell the deleted tree from an intact one
+# proves nothing -- an earlier lane's "green" run turned out to have measured
+# the INTACT tree, because BUNDLE_GEMFILE pointed at the gemspec, which
+# unshifts the real lib/ onto $LOAD_PATH ahead of any -I. Here the subject IS
+# the real lib/, so the trap is the opposite one: the probe must prove it
+# loaded THIS repo's lib/ and not some installed copy of the gem. It asserts
+# that before it asserts any absence -- otherwise "the constant is gone" and
+# "nothing was ever loaded" look identical.
+class RemovedSurfaceTest < ActiveSupport::TestCase
+ LIB = Pathname.new(File.expand_path("../../lib", __dir__))
+
+ # The 2.1 `git rm`, verbatim -- the IMPLEMENTATION files. Anything on this
+ # list reappearing under lib/ is a shipped regression, not a local mess.
+ #
+ # The line moved on 2026-08-24 (ADR-008 amendment) and it moved by exactly
+ # one category: the v1 REQUIRE PATHS are back as one-line alias entries
+ # (lib/capybara/screenshot/diff.rb, lib/capybara_screenshot_diff{,/*}.rb),
+ # because four of six discoverable real users import the gem under them and
+ # a LoadError there fires before any constant alias could help. What those
+ # files must never contain again is what is listed here: the v1 tree that
+ # held logic, the deprecation channel, and the driver abstraction.
+ # compat_surface_test pins the entries; this pins their emptiness.
+ REMOVED_PATHS = %w[
+ capybara/screenshot/diff
+ capybara_screenshot_diff/snap.rb
+ capybara_screenshot_diff/snap_manager.rb
+ capybara_screenshot_diff/screenshot_assertion.rb
+ capybara_screenshot_diff/screenshot_namer.rb
+ capybara_screenshot_diff/attempts_reporter.rb
+ capybara_screenshot_diff/static.rb
+ capybara_screenshot_diff/reporters
+ capybara_screenshot_diff/error_with_filtered_backtrace.rb
+ snap_diff/legacy_shims.rb
+ snap_diff/deprecation.rb
+ snap_diff/removal.rb
+ snap_diff/driver.rb
+ snap_diff/drivers.rb
+ snap_diff/drivers/chunky_png_driver.rb
+ snap_diff/utils.rb
+ ].freeze
+
+ # Alias-only by contract: three lines of comment and one `require`. A v1
+ # entry file that grows a `def` or a constant assignment has stopped being
+ # an alias and started being the tree 2.1 deleted.
+ ALIAS_ENTRIES = %w[
+ capybara/screenshot/diff.rb
+ capybara_screenshot_diff.rb
+ capybara_screenshot_diff/dsl.rb
+ capybara_screenshot_diff/minitest.rb
+ capybara_screenshot_diff/rspec.rb
+ capybara_screenshot_diff/cucumber.rb
+ ].freeze
+
+ # NOT removed, and deliberately absent from the list above:
+ # lib/capybara-screenshot-diff.rb. It looks like part of the v1 tree and is
+ # not -- it is the Bundler entry point for the `capybara-screenshot-diff`
+ # GEM NAME, which is still published (both names ship identical content).
+ # support_load_probe_test pins both gem-name entries.
+
+ # Removed CONSTANTS, by fully qualified name.
+ #
+ # Capybara::Screenshot and CapybaraScreenshotDiff are NOT here since the
+ # ADR-008 amendment: they survive as permanent eager same-object aliases of
+ # SnapDiff (snap_diff/compat.rb), pinned by compat_surface_test. What is
+ # listed here is the machinery those names used to carry.
+ REMOVED_CONSTANTS = %w[
+ SnapDiff::Deprecation
+ SnapDiff::Removal
+ SnapDiff::Driver
+ SnapDiff::Utils
+ SnapDiff::Drivers::ChunkyPNGDriver
+ SnapDiff::Drivers::AVAILABLE_DRIVERS
+ ].freeze
+
+ # Removed METHODS on SnapDiff itself. `start` yielded the two v1 config
+ # holders (SnapDiff.configure replaces it); `silence_deprecations` silenced
+ # a channel that no longer exists.
+ REMOVED_METHODS = %w[start silence_deprecations silence_deprecations=].freeze
+
+ # Removed driver-registry methods. Named separately because SnapDiff::Drivers
+ # SURVIVES as the namespace SnapDiff::Drivers::VipsDriver is published under
+ # -- so "the module is gone" would be the wrong assertion.
+ REMOVED_DRIVERS_METHODS = %w[loaded available for registry detect_available].freeze
+
+ # `driver` / `shift_distance_limit` used to be listed here as removed
+ # settings whose absence was asserted. The ADR-008 amendment reversed that:
+ # deleting a setting a real config writes is a SILENT failure (one known
+ # user guards the writer with `respond_to?`), so they are raising stubs now
+ # rather than gone. compat_surface_test owns them.
+
+ # Runs FIRST, before any absence assertion. Proves the process really loaded
+ # THIS repo's lib/ -- otherwise every "constant is gone" below is vacuous.
+ GATE_SCRIPT = <<~'RUBY'
+ lib = ENV.fetch("LIB_UNDER_TEST")
+ gate = []
+
+ loaded = $LOADED_FEATURES.grep(%r{/lib/snap_diff(/|\.rb\z)})
+ gate << "no snap_diff files loaded at all" if loaded.empty?
+
+ # The BUNDLE_GEMFILE trap in reverse: files resolving from an INSTALLED
+ # copy of the gem while the repo's lib/ sits unused on the load path.
+ #
+ # Anchored on the library path, NOT a bare /snap_diff/ substring: CI checks
+ # this repo out at .../snap_diff-capybara/, so a bare match flags every gem
+ # under vendor/bundle and the gate fails everywhere except a dev machine
+ # whose directory happens to be named otherwise.
+ strays = loaded.reject { |f| f.start_with?(lib) }
+ gate << "loaded from outside the tree under test: #{strays.join(", ")}" unless strays.empty?
+
+ # A positive control: something the gem still HAS must be present, or the
+ # process is too broken for an absence to mean anything.
+ gate << "SnapDiff.configure is missing -- the gem did not load" unless SnapDiff.respond_to?(:configure)
+ gate << "VipsDriver is missing -- the only backend did not load" unless
+ defined?(SnapDiff::Drivers::VipsDriver)
+
+ unless gate.empty?
+ abort("GATE: this process is NOT measuring the repo's lib/, so nothing below is evidence:\n- " + gate.join("\n- "))
+ end
+ RUBY
+
+ test "no removed path is back under lib/" do
+ back = REMOVED_PATHS.select { |path| LIB.join(path).exist? }
+
+ assert_empty back, <<~MSG
+ Path(s) 2.1 removed exist under lib/ again. Everything under lib/ is
+ packaged, so this SHIPS:
+
+ #{back.join("\n")}
+ MSG
+ end
+
+ test "a fresh process loading the gem defines none of the removed names" do
+ failure = probe(<<~RUBY)
+ require "snap_diff"
+ #{GATE_SCRIPT}
+
+ back = []
+ #{REMOVED_CONSTANTS.inspect}.each { |c| back << c if Object.const_defined?(c) }
+ #{REMOVED_METHODS.inspect}.each { |m| back << "SnapDiff.\#{m}" if SnapDiff.respond_to?(m) }
+ #{REMOVED_DRIVERS_METHODS.inspect}.each do |m|
+ back << "SnapDiff::Drivers.\#{m}" if SnapDiff::Drivers.respond_to?(m)
+ end
+ abort("still defined: \#{back.join(", ")}") unless back.empty?
+ RUBY
+
+ assert_nil failure, <<~MSG
+ A name 2.1 removed is defined again in a fresh process:
+
+ #{failure}
+ MSG
+ end
+
+ # The gate line has to be able to FAIL, or it is a comment with an `if`
+ # around it. Same script, run with the repo's lib/ NOT on the load path and
+ # LIB_UNDER_TEST still pointing at it: every absence assertion would pass
+ # there (nothing is loaded, so nothing is defined), so only the gate can
+ # reject it.
+ test "the gate line rejects a process that never loaded the tree under test" do
+ script = <<~RUBY
+ module SnapDiff
+ def self.configure = nil
+ end
+ #{GATE_SCRIPT}
+ RUBY
+ env = {"LIB_UNDER_TEST" => LIB.to_s, "RUBYOPT" => nil, "BUNDLE_GEMFILE" => nil, "RUBYLIB" => nil}
+ out, status = Open3.capture2e(env, RbConfig.ruby, "-e", script, chdir: Dir.tmpdir)
+
+ assert_not status.success?, "the gate line passed on a process that loaded nothing"
+ assert_includes out, "no snap_diff files loaded at all"
+ end
+
+ test "the v1 require paths are alias entries, not the tree that was deleted" do
+ logic = ALIAS_ENTRIES.filter_map do |entry|
+ file = LIB.join(entry)
+ next "#{entry}: missing -- a real user's `require` line now LoadErrors" unless file.exist?
+
+ code = file.read.lines.map(&:strip).reject { |line| line.empty? || line.start_with?("#") }
+ offending = code.grep_v(/\Arequire /)
+ "#{entry}: #{offending.join(" / ")}" unless offending.empty?
+ end
+
+ assert_empty logic, <<~MSG
+ A v1 entry file is missing, or has grown something other than a
+ `require`. These are alias entries: they exist so a real user's require
+ line resolves, not to hold the surface 2.1 deleted.
+
+ #{logic.join("\n")}
+ MSG
+ end
+
+ # --- The release pipeline is a consumer of this deletion too ---------
+ #
+ # release.yml verified the version by executing
+ # `ruby -I lib -r capybara/screenshot/diff/version -e "puts
+ # Capybara::Screenshot::Diff::VERSION"`. 2.1 deleted that file, so the FIRST
+ # 2.1 release would have failed at "Verify version" -- a release workflow
+ # broken by the release it is releasing, discovered at the worst moment.
+ #
+ # Generic on purpose: it extracts every inline `ruby -I lib -r ... -e ...`
+ # from .github and RUNS it, so the next one is covered without an edit here.
+ WORKFLOW_RUBY = /ruby -I lib -r (\S+) -e "([^"]*)"/
+
+ test "every inline ruby the CI workflows run still loads and prints what it claims" do
+ invocations = github_files.flat_map do |rel, file|
+ file.read.scan(WORKFLOW_RUBY).map { |path, script| [rel, path, script] }
+ end
+
+ assert_not_empty invocations, "no inline ruby found in .github -- this gate would pass vacuously"
+
+ failures = invocations.filter_map do |rel, path, script|
+ out, status = Open3.capture2e(
+ {"RUBYOPT" => nil, "BUNDLE_GEMFILE" => nil},
+ RbConfig.ruby, "-I", "lib", "-r", path, "-e", script, chdir: PROJECT_ROOT.to_s
+ )
+ "#{rel}: `ruby -I lib -r #{path} -e \"#{script}\"` -> #{out.strip}" unless status.success?
+ end
+
+ assert_empty failures, <<~MSG
+ A workflow executes ruby against something this release removed. It
+ fails on the release it is releasing:
+
+ #{failures.join("\n")}
+ MSG
+ end
+
+ test "the release workflow verifies the version against the namespace the gem still ships" do
+ workflow = PROJECT_ROOT.join(".github/workflows/release.yml").read
+ command = workflow[/CODE_VERSION=\$\(([^)]+)\)/, 1]
+
+ assert command, "release.yml no longer computes CODE_VERSION the way this gate reads it"
+
+ out, status = Open3.capture2e(
+ {"RUBYOPT" => nil, "BUNDLE_GEMFILE" => nil},
+ "/bin/sh", "-c", command, chdir: PROJECT_ROOT.to_s
+ )
+
+ assert status.success?, "release.yml's version check does not run: #{out}"
+ # Last line only: the shell resolves `ruby` through whatever shim the
+ # developer's version manager installed, and some of them chatter first.
+ assert_equal SnapDiff::VERSION, out.lines.last.to_s.strip
+ end
+
+ # The non-executable half: a workflow, action or issue template that merely
+ # NAMES something removed is not caught above and quietly misinforms.
+ # `capybara_screenshot_diff/` is excluded from the alternation -- those four
+ # require paths survive as alias entries (see ALIAS_ENTRIES).
+ GITHUB_REMOVED_MENTIONS = %r{
+ capybara/screenshot/diff/
+ |chunky_?png
+ |SCREENSHOT_DRIVER
+ |shift_distance_limit
+ }xi
+
+ test "nothing under .github names a file or setting this release removed" do
+ files = github_files
+ assert_not_empty files, "the .github scan matched nothing -- it would pass vacuously"
+
+ offenders = files.flat_map do |rel, file|
+ file.read.lines.each_with_index.filter_map do |line, index|
+ next if line.lstrip.start_with?("#")
+
+ "#{rel}:#{index + 1}: #{line.strip}" if GITHUB_REMOVED_MENTIONS.match?(line)
+ end
+ end
+
+ assert_empty offenders, <<~MSG
+ CI configuration names something 2.1 removed. Nothing executes these
+ lines, so nothing else will ever say they are wrong:
+
+ #{offenders.join("\n")}
+ MSG
+ end
+
+ private
+
+ PROJECT_ROOT = Pathname.new(File.expand_path("../..", __dir__))
+
+ # [relative path, Pathname] for every file under .github/.
+ def github_files
+ Dir[".github/**/*", base: PROJECT_ROOT.to_s]
+ .map { |rel| [rel, PROJECT_ROOT.join(rel)] }
+ .select { |_rel, file| file.file? }
+ end
+
+ # A fresh process with ONLY the repo's lib/ on the load path.
+ #
+ # `chdir: Dir.tmpdir` is the load-bearing half, and NOT a detail. Scrubbing
+ # RUBYOPT/BUNDLE_GEMFILE is not sufficient on its own: with the cwd still
+ # inside the project, RubyGems auto-discovers gems.rb, puts `-rbundler/setup`
+ # BACK into RUBYOPT, and the gemspec unshifts lib/ ahead of the -I dir.
+ # Running from a tmpdir means there is no gems.rb to find. Both defenses are
+ # here because the gate line inside the script is the only thing that says so
+ # out loud when they stop working.
+ def probe(script)
+ env = {"LIB_UNDER_TEST" => LIB.to_s, "RUBYOPT" => nil, "BUNDLE_GEMFILE" => nil, "RUBYLIB" => nil}
+ out, status = Open3.capture2e(
+ env, RbConfig.ruby, "-I#{LIB}", "-e", script, chdir: Dir.tmpdir
+ )
+
+ out unless status.success?
+ end
+end
diff --git a/test/unit/screenshoter_test.rb b/test/unit/screenshoter_test.rb
index 8012c106..7f3a59d2 100644
--- a/test/unit/screenshoter_test.rb
+++ b/test/unit/screenshoter_test.rb
@@ -8,7 +8,7 @@ class ScreenshoterTest < ActiveSupport::TestCase
include DSLStub
test "#take_screenshot without wait skips image loading" do
- screenshoter = SnapDiff::Screenshoter.new({wait: nil}, {driver: :chunky_png})
+ screenshoter = SnapDiff::Screenshoter.new({wait: nil})
mock = ::Minitest::Mock.new
mock.expect(:save_screenshot, true) { |path| path.include?("01_a.png") }
@@ -23,10 +23,7 @@ class ScreenshoterTest < ActiveSupport::TestCase
end
test "#take_screenshot with custom screenshot options" do
- screenshoter = SnapDiff::Screenshoter.new(
- {wait: nil, capybara_screenshot_options: {full: true}},
- {driver: :chunky_png}
- )
+ screenshoter = SnapDiff::Screenshoter.new({wait: nil, capybara_screenshot_options: {full: true}})
mock = ::Minitest::Mock.new
mock.expect(:save_screenshot, true) { |path, options| path.include?("01_a.png") && options[:full] }
@@ -41,14 +38,13 @@ class ScreenshoterTest < ActiveSupport::TestCase
end
test "#prepare_page_for_screenshot without wait does not raise any error" do
- screenshoter = SnapDiff::Screenshoter.new({wait: nil}, {driver: :chunky_png})
+ screenshoter = SnapDiff::Screenshoter.new({wait: nil})
assert_nil screenshoter.prepare_page_for_screenshot(timeout: nil) # does not raise an error
end
- test "#resize_if_needed halves a non-square retina screenshot to the expected window size via VipsDriver" do
- skip "VIPS not present. Skipping VIPS driver tests." unless defined?(Vips)
- screenshoter = SnapDiff::Screenshoter.new({}, {driver: :vips})
+ test "#resize_if_needed halves a non-square retina screenshot to the expected window size" do
+ screenshoter = SnapDiff::Screenshoter.new({})
retina_image = Vips::Image.black(2560, 1600) # 2x window size, non-square
resized = SnapDiff.config.stub(:window_size, [1280, 1024]) do
diff --git a/test/unit/snap_diff_config_test.rb b/test/unit/snap_diff_config_test.rb
index 4608d373..f9d2e553 100644
--- a/test/unit/snap_diff_config_test.rb
+++ b/test/unit/snap_diff_config_test.rb
@@ -124,28 +124,21 @@ def config
config.save_path = original_save_path
end
- # The vips tolerance floor in Config#default_options is the one literal in
- # there that is not a stored setting, and deleting it left the full suite
- # green (config.rb's then-arm had a hit count of 0): nothing ever asked for
- # default_options with driver == :vips and no explicit tolerance.
- test "default_options floors tolerance at 0.001 for vips and only for vips" do
- original_driver, original_tolerance = config.driver, config.tolerance
+ # The tolerance floor in Config#default_options is the one literal in there
+ # that is not a stored setting. It used to be conditional on
+ # `driver == :vips`; 2.1 made libvips the only backend, so the condition was
+ # always true and is gone. The "and only for vips" half of this test went
+ # with the `driver` setting.
+ test "default_options floors tolerance at 0.001, and an explicit tolerance still wins" do
+ original_tolerance = config.tolerance
begin
config.tolerance = nil
-
- config.driver = :vips
assert_equal 0.001, config.default_options[:tolerance]
- config.driver = :chunky_png
- assert_nil config.default_options[:tolerance]
-
- # An explicit tolerance still wins over the floor.
config.tolerance = 0.5
- config.driver = :vips
assert_equal 0.5, config.default_options[:tolerance]
ensure
- config.driver = original_driver
config.tolerance = original_tolerance
end
end
diff --git a/test/unit/support_load_probe_test.rb b/test/unit/support_load_probe_test.rb
index dda92bc8..30e8bb1a 100644
--- a/test/unit/support_load_probe_test.rb
+++ b/test/unit/support_load_probe_test.rb
@@ -78,10 +78,13 @@ class SupportLoadProbeTest < ActiveSupport::TestCase
config configure compare session reset pending_screenshots_message
].freeze
- # snap_diff-capybara is the canonical gem's Bundler entry point, so it is
- # probed here rather than with the v1 ones: 2.1 keeps it (repointed at
- # snap_diff/integrations/minitest), it just stops carrying the
- # CapybaraScreenshotDiff surface.
+ # Both GEM NAMES have a Bundler entry point, and both are probed here rather
+ # than with the v1 ones: 2.1 keeps them (repointed at
+ # snap_diff/integrations/minitest), they just stop carrying the
+ # CapybaraScreenshotDiff surface. capybara-screenshot-diff is the name most
+ # existing users have in their Gemfile -- if its entry file goes missing,
+ # `Bundler.require` is a silent no-op and the first SnapDiff reference is a
+ # confusing NameError, so it is pinned here deliberately.
CANONICAL_ENTRY_POINTS = {
"snap_diff" => CANONICAL_SURFACE,
"snap_diff/dsl" => CANONICAL_SURFACE,
@@ -89,7 +92,8 @@ class SupportLoadProbeTest < ActiveSupport::TestCase
"snap_diff/integrations/rspec" => CANONICAL_SURFACE,
"snap_diff/integrations/cucumber" => CANONICAL_SURFACE,
"snap_diff/static" => CANONICAL_SURFACE + %w[serve],
- "snap_diff-capybara" => CANONICAL_SURFACE
+ "snap_diff-capybara" => CANONICAL_SURFACE,
+ "capybara-screenshot-diff" => CANONICAL_SURFACE
}.freeze
test "every canonical snap_diff entry point loads the full SnapDiff surface" do