Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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?
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
25 changes: 10 additions & 15 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,6 @@ jobs:
ruby-version: "4.0"

- run: bin/rake test
env:
SCREENSHOT_DRIVER: vips

functional-test:
name: Functional Test
Expand All @@ -81,7 +79,6 @@ jobs:
env:
COVERAGE: enabled
DISABLE_SKIP_TESTS: 1
SCREENSHOT_DRIVER: vips

- uses: ./.github/actions/upload-screenshots
if: failure()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -188,7 +184,6 @@ jobs:
strategy:
matrix:
capybara-driver: [ selenium_headless, selenium_chrome_headless, cuprite ]
screenshot-driver: [ vips, chunky_png ]

runs-on: ubuntu-latest

Expand All @@ -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
Expand All @@ -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
Expand Down
31 changes: 10 additions & 21 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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`)

Expand Down Expand Up @@ -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)
Expand Down
51 changes: 27 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,38 @@
[![Test](https://github.com/snap-diff/snap_diff-capybara/actions/workflows/test.yml/badge.svg)](https://github.com/snap-diff/snap_diff-capybara/actions/workflows/test.yml)
[![DeepWiki](https://img.shields.io/badge/DeepWiki-snap--diff%2Fsnap__diff--capybara-blue.svg?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IndoaXRlIiBzdHJva2Utd2lkdGg9IjIiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCI+PHBhdGggZD0iTTEyIDJhMTAgMTAgMCAxIDAgMCAyMCAxMCAxMCAwIDAgMCAwLTIweiIvPjxwYXRoIGQ9Ik0xMiA2djEyIi8+PHBhdGggZD0iTTYgMTJoMTIiLz48L3N2Zz4=)](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.

[![SnapDiff Web UI](docs/images/snap_diff_web_ui.png)](#web-ui-for-reviewing-screenshot-changes)

**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)

> Already using Capybara for system tests? Add the gem and you're ready. New to system tests? See [Rails System Testing guide](https://guides.rubyonrails.org/testing.html#system-testing).

```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
```

Expand Down Expand Up @@ -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).
Expand All @@ -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.
Expand All @@ -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`:
Expand All @@ -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
```
Expand All @@ -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

Expand All @@ -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
```

Expand All @@ -174,7 +177,7 @@ Delete the baseline file and re-run tests: `rm doc/screenshots/homepage.png && b
<details>
<summary><strong>CSS animations make my screenshots flaky</strong></summary>

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.
</details>

<details>
Expand All @@ -186,7 +189,7 @@ Set `window_size` for consistent dimensions and use `perceptual_threshold: 2.0`
<details>
<summary><strong>Will this slow down my tests?</strong></summary>

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.
</details>

<details>
Expand All @@ -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

Expand All @@ -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

Expand Down
Loading