From f138705dfb6596fbdd114cd25fb3ff39af8a641d Mon Sep 17 00:00:00 2001 From: Paul Keen <125715+pftg@users.noreply.github.com> Date: Sun, 23 Aug 2026 20:31:29 +0200 Subject: [PATCH 1/6] feat!: 2.1 removes the legacy namespace trees and the driver abstraction Deletes everything 2.0 announced as removed in 2.1, in one release: - The v1 namespace trees (lib/capybara/, lib/capybara_screenshot_diff/, the two gem-name entry points, legacy_shims.rb, deprecation.rb, test/legacy/). SnapDiff.configure is the single config entry point; SnapDiff.start yielded the two v1 holders and could not outlive them. - The chunky_png driver and shift_distance_limit (chunky-only, no libvips equivalent). - The whole driver abstraction: the SnapDiff::Driver mixin, the SnapDiff::Drivers registry (.loaded/.available/.for/.registry/ .detect_available), AVAILABLE_DRIVERS, Utils.detect_available_drivers, and :auto selection. Comparison and Screenshoter construct Drivers::VipsDriver directly. - The `driver:` config setting. With one backend it cannot select anything, and accept-and-ignore would let a config claim a backend choice that does not exist. - snap_diff/removal.rb, which existed only to warn about the above. SnapDiff.silence_deprecations goes with it: no channel is left. ruby-vips becomes a real gemspec runtime dependency (>= 2.0, < 3), so a missing binding is a resolver error rather than a runtime one. Fixes a latent bug the deletion exposed: libvips caches loader operations on filename + mtime, and mtime has one-second resolution, so rewriting a screenshot path and re-reading it within the same second served the PREVIOUS image. It was masked because a Comparison built without an explicit driver defaulted to chunky_png, which always re-read the file. VipsDriver#from_file now passes `revalidate: true`; the cache-flush workarounds in system_test_case.rb and vips_driver_test.rb are gone. Gates: legacy_deletion_test (simulated a deletion that has happened) and legacy_tree_is_alias_only_test (its subject is gone) are replaced by removed_surface_test, which asserts absence -- no removed path back under lib/, no removed name defined in a fresh process -- behind a gate line that proves it measured this repo's lib/. The two reverse gates survive, repurposed to catch removed names in strings and docstrings. test:canonical was "everything except test/legacy/", which is now exactly test; the tasks converged and the second name is gone. `rake test` is THE gate. test:benchmark is deleted (it required a script not in this repo). --- .github/workflows/test.yml | 25 +- README.md | 9 +- Rakefile | 56 +-- bin/console | 2 +- bin/dtest | 2 +- capybara-screenshot-diff.gemspec | 9 + docs/architecture.md | 60 ++- docs/configuration.md | 68 ++-- docs/drivers.md | 111 ++---- docs/snapdiff.md | 81 +--- gems.rb | 6 +- lib/capybara-screenshot-diff.rb | 3 - lib/capybara/screenshot/diff.rb | 3 - .../screenshot/diff/annotation_service.rb | 5 - .../screenshot/diff/area_calculator.rb | 5 - .../screenshot/diff/browser_helpers.rb | 5 - lib/capybara/screenshot/diff/config_legacy.rb | 21 - lib/capybara/screenshot/diff/cucumber.rb | 7 - lib/capybara/screenshot/diff/difference.rb | 5 - lib/capybara/screenshot/diff/drivers.rb | 8 - .../screenshot/diff/drivers/base_driver.rb | 9 - .../diff/drivers/chunky_png_driver.rb | 7 - .../screenshot/diff/drivers/vips_driver.rb | 6 - lib/capybara/screenshot/diff/image_compare.rb | 16 - .../screenshot/diff/image_preprocessor.rb | 5 - lib/capybara/screenshot/diff/os.rb | 7 - lib/capybara/screenshot/diff/region.rb | 5 - .../screenshot/diff/reporters/default.rb | 8 - .../screenshot/diff/screenshot_matcher.rb | 5 - lib/capybara/screenshot/diff/screenshoter.rb | 5 - .../screenshot/diff/stable_screenshoter.rb | 5 - lib/capybara/screenshot/diff/utils.rb | 5 - lib/capybara/screenshot/diff/vcs.rb | 5 - lib/capybara/screenshot/diff/version.rb | 10 - lib/capybara_screenshot_diff.rb | 45 --- .../attempts_reporter.rb | 5 - lib/capybara_screenshot_diff/cucumber.rb | 8 - lib/capybara_screenshot_diff/dsl.rb | 12 - .../error_with_filtered_backtrace.rb | 5 - lib/capybara_screenshot_diff/minitest.rb | 14 - .../reporters/html.rb | 5 - lib/capybara_screenshot_diff/rspec.rb | 8 - .../screenshot_assertion.rb | 59 --- .../screenshot_namer.rb | 5 - lib/capybara_screenshot_diff/snap.rb | 5 - lib/capybara_screenshot_diff/snap_manager.rb | 5 - lib/capybara_screenshot_diff/static.rb | 13 - lib/snap_diff-capybara.rb | 5 +- lib/snap_diff.rb | 34 +- lib/snap_diff/comparison.rb | 17 +- lib/snap_diff/config.rb | 54 +-- lib/snap_diff/deprecation.rb | 132 ------- lib/snap_diff/driver.rb | 55 --- lib/snap_diff/drivers.rb | 116 ------ lib/snap_diff/drivers/chunky_png_driver.rb | 298 -------------- lib/snap_diff/drivers/vips_driver.rb | 48 ++- lib/snap_diff/dsl.rb | 12 +- lib/snap_diff/image_preprocessor.rb | 23 +- lib/snap_diff/legacy_shims.rb | 366 ------------------ lib/snap_diff/removal.rb | 115 ------ lib/snap_diff/screenshot_matcher.rb | 1 - lib/snap_diff/screenshoter.rb | 8 +- lib/snap_diff/utils.rb | 58 --- scripts/generate_sample_report.rb | 20 +- .../rspec_after_hook_order_masking_spec.rb | 1 - test/fixtures/rspec_pending_masking_spec.rb | 1 - test/fixtures/rspec_spec.rb | 3 +- test/integration/browser_screenshot_test.rb | 4 +- test/integration/record_screenshot_test.rb | 6 +- test/legacy/errors_alias_test.rb | 54 --- test/legacy/legacy_config_accessors_test.rb | 196 ---------- .../legacy_config_default_timing_test.rb | 58 --- test/legacy/legacy_entry_point_probe_test.rb | 286 -------------- test/legacy/legacy_forwarders_test.rb | 124 ------ .../legacy_namespace_deprecation_test.rb | 166 -------- test/legacy/legacy_tree_is_alias_only_test.rb | 162 -------- test/legacy/namespace_forwarding_test.rb | 153 -------- test/legacy/snap_diff_deprecation_test.rb | 236 ----------- test/support/driver_contract_tests.rb | 33 +- test/support/driver_coverage.rb | 33 -- test/system_test_case.rb | 4 +- test/test_helper.rb | 58 +-- test/unit/attempts_reporter_test.rb | 6 +- ...canonical_suite_has_no_legacy_refs_test.rb | 104 ++--- test/unit/compare_api_test.rb | 15 +- test/unit/config_default_timing_test.rb | 2 - .../unit/core_tree_has_no_legacy_deps_test.rb | 58 +-- test/unit/diff_test.rb | 13 +- test/unit/driver_coverage_test.rb | 40 -- test/unit/drivers/chunky_png_driver_test.rb | 161 -------- test/unit/drivers/utils_test.rb | 52 --- test/unit/drivers/vips_driver_test.rb | 16 +- test/unit/drivers_test.rb | 91 ----- test/unit/dsl_test.rb | 37 +- test/unit/image_compare_test.rb | 126 ++---- test/unit/image_preprocessor_test.rb | 29 +- test/unit/legacy_deletion_test.rb | 201 ---------- test/unit/removed_in_2_1_deprecation_test.rb | 249 ------------ test/unit/removed_surface_test.rb | 180 +++++++++ test/unit/screenshoter_test.rb | 14 +- test/unit/snap_diff_config_test.rb | 21 +- 101 files changed, 648 insertions(+), 4485 deletions(-) delete mode 100644 lib/capybara-screenshot-diff.rb delete mode 100644 lib/capybara/screenshot/diff.rb delete mode 100644 lib/capybara/screenshot/diff/annotation_service.rb delete mode 100644 lib/capybara/screenshot/diff/area_calculator.rb delete mode 100644 lib/capybara/screenshot/diff/browser_helpers.rb delete mode 100644 lib/capybara/screenshot/diff/config_legacy.rb delete mode 100644 lib/capybara/screenshot/diff/cucumber.rb delete mode 100644 lib/capybara/screenshot/diff/difference.rb delete mode 100644 lib/capybara/screenshot/diff/drivers.rb delete mode 100644 lib/capybara/screenshot/diff/drivers/base_driver.rb delete mode 100644 lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb delete mode 100644 lib/capybara/screenshot/diff/drivers/vips_driver.rb delete mode 100644 lib/capybara/screenshot/diff/image_compare.rb delete mode 100644 lib/capybara/screenshot/diff/image_preprocessor.rb delete mode 100644 lib/capybara/screenshot/diff/os.rb delete mode 100644 lib/capybara/screenshot/diff/region.rb delete mode 100644 lib/capybara/screenshot/diff/reporters/default.rb delete mode 100644 lib/capybara/screenshot/diff/screenshot_matcher.rb delete mode 100644 lib/capybara/screenshot/diff/screenshoter.rb delete mode 100644 lib/capybara/screenshot/diff/stable_screenshoter.rb delete mode 100644 lib/capybara/screenshot/diff/utils.rb delete mode 100644 lib/capybara/screenshot/diff/vcs.rb delete mode 100644 lib/capybara/screenshot/diff/version.rb delete mode 100644 lib/capybara_screenshot_diff.rb delete mode 100644 lib/capybara_screenshot_diff/attempts_reporter.rb delete mode 100644 lib/capybara_screenshot_diff/cucumber.rb delete mode 100644 lib/capybara_screenshot_diff/dsl.rb delete mode 100644 lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb delete mode 100644 lib/capybara_screenshot_diff/minitest.rb delete mode 100644 lib/capybara_screenshot_diff/reporters/html.rb delete mode 100644 lib/capybara_screenshot_diff/rspec.rb delete mode 100644 lib/capybara_screenshot_diff/screenshot_assertion.rb delete mode 100644 lib/capybara_screenshot_diff/screenshot_namer.rb delete mode 100644 lib/capybara_screenshot_diff/snap.rb delete mode 100644 lib/capybara_screenshot_diff/snap_manager.rb delete mode 100644 lib/capybara_screenshot_diff/static.rb delete mode 100644 lib/snap_diff/deprecation.rb delete mode 100644 lib/snap_diff/driver.rb delete mode 100644 lib/snap_diff/drivers.rb delete mode 100644 lib/snap_diff/drivers/chunky_png_driver.rb delete mode 100644 lib/snap_diff/legacy_shims.rb delete mode 100644 lib/snap_diff/removal.rb delete mode 100644 lib/snap_diff/utils.rb delete mode 100644 test/legacy/errors_alias_test.rb delete mode 100644 test/legacy/legacy_config_accessors_test.rb delete mode 100644 test/legacy/legacy_config_default_timing_test.rb delete mode 100644 test/legacy/legacy_entry_point_probe_test.rb delete mode 100644 test/legacy/legacy_forwarders_test.rb delete mode 100644 test/legacy/legacy_namespace_deprecation_test.rb delete mode 100644 test/legacy/legacy_tree_is_alias_only_test.rb delete mode 100644 test/legacy/namespace_forwarding_test.rb delete mode 100644 test/legacy/snap_diff_deprecation_test.rb delete mode 100644 test/support/driver_coverage.rb delete mode 100644 test/unit/driver_coverage_test.rb delete mode 100644 test/unit/drivers/chunky_png_driver_test.rb delete mode 100644 test/unit/drivers/utils_test.rb delete mode 100644 test/unit/drivers_test.rb delete mode 100644 test/unit/legacy_deletion_test.rb delete mode 100644 test/unit/removed_in_2_1_deprecation_test.rb create mode 100644 test/unit/removed_surface_test.rb 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/README.md b/README.md index 909e337a..181876e5 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,8 @@ 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 +# ruby-vips comes with the gem since 2.1; libvips itself is a system package +# (brew install vips / apt-get install libvips). ``` ```ruby @@ -186,7 +187,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,7 +198,7 @@ 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 `CapybaraScreenshotDiff.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 @@ -205,7 +206,7 @@ Comparisons add ~50ms per image with VIPS. Without `ruby-vips`, ChunkyPNG is use - [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 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/architecture.md b/docs/architecture.md index dc864310..a2a9466a 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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 │ │ +│ └────────────────────────────────────┘ │ └──────────────────────────────────────────┘ ``` @@ -108,41 +107,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: @@ -236,7 +229,7 @@ The two legacy views are organized into two namespaces: - `driver`, `tolerance`, `color_distance_limit`, `perceptual_threshold`, `shift_distance_limit` - `area_size_limit`, `skip_area`, `fail_if_new`, `fail_on_difference`, `delayed` -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. +The canonical way in is `SnapDiff.configure { |config| ... }` (all 25 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. `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. @@ -249,18 +242,13 @@ lib/ snap_diff.rb # SnapDiff module: compare/start/configure/config snap_diff/ # Canonical implementation (v2) 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 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 diff --git a/docs/configuration.md b/docs/configuration.md index 2406f5ed..2fc1b5c6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -11,7 +11,6 @@ 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 @@ -52,13 +51,13 @@ exception: `Capybara::Screenshot.enabled` is `SnapDiff.config.screenshot_enabled ## 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 +65,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 +86,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 +95,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 ``` @@ -122,7 +119,6 @@ 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) | @@ -311,41 +307,21 @@ Capybara::Screenshot::Diff.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`. - -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: +### Allowed shift distance — removed in 2.1 -```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 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.