snap_diff-capybara 2.0.0.alpha1 → 2.0.0.beta2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -0
  3. data/docs/UPGRADING.md +243 -1
  4. data/docs/architecture.md +91 -53
  5. data/docs/ci-integration.md +13 -3
  6. data/docs/configuration.md +31 -2
  7. data/docs/drivers.md +6 -0
  8. data/docs/framework-setup.md +21 -3
  9. data/docs/reporters.md +20 -5
  10. data/docs/snapdiff.md +326 -0
  11. data/docs/thread_safety.md +4 -3
  12. data/lib/capybara/screenshot/diff/config_legacy.rb +26 -72
  13. data/lib/capybara/screenshot/diff/image_compare.rb +5 -2
  14. data/lib/capybara/screenshot/diff/region.rb +3 -105
  15. data/lib/capybara/screenshot/diff/reporters/default.rb +4 -106
  16. data/lib/capybara_screenshot_diff/screenshot_assertion.rb +12 -24
  17. data/lib/capybara_screenshot_diff.rb +10 -4
  18. data/lib/snap_diff/area_calculator.rb +1 -3
  19. data/lib/snap_diff/browser_helpers.rb +1 -3
  20. data/lib/snap_diff/capture/viewport.rb +6 -7
  21. data/lib/snap_diff/comparison.rb +15 -28
  22. data/lib/snap_diff/config.rb +151 -29
  23. data/lib/snap_diff/deprecation.rb +26 -8
  24. data/lib/snap_diff/drivers.rb +20 -0
  25. data/lib/snap_diff/dsl.rb +12 -12
  26. data/lib/snap_diff/errors.rb +19 -0
  27. data/lib/snap_diff/integrations/cucumber.rb +5 -4
  28. data/lib/snap_diff/integrations/minitest.rb +11 -12
  29. data/lib/snap_diff/integrations/rspec.rb +7 -6
  30. data/lib/snap_diff/legacy_shims.rb +18 -0
  31. data/lib/snap_diff/region.rb +117 -0
  32. data/lib/snap_diff/reporters/default.rb +107 -0
  33. data/lib/snap_diff/reporters/html.rb +7 -9
  34. data/lib/snap_diff/reporting.rb +13 -4
  35. data/lib/snap_diff/screenshot_assertion.rb +37 -4
  36. data/lib/snap_diff/screenshot_matcher.rb +4 -4
  37. data/lib/snap_diff/screenshoter.rb +1 -1
  38. data/lib/snap_diff/snap_manager.rb +1 -2
  39. data/lib/snap_diff/stable_screenshoter.rb +2 -2
  40. data/lib/snap_diff/utils.rb +5 -3
  41. data/lib/snap_diff/version.rb +1 -1
  42. data/lib/snap_diff.rb +44 -15
  43. metadata +5 -1
@@ -4,10 +4,19 @@
4
4
 
5
5
  ```ruby
6
6
  # test/test_helper.rb
7
- require 'capybara_screenshot_diff/static'
7
+ require 'snap_diff/static'
8
+
9
+ SnapDiff.serve("_site") # or "public", "build", "dist"
10
+ ```
11
+
12
+ <details>
13
+ <summary>Legacy names (still supported)</summary>
8
14
 
9
- CapybaraScreenshotDiff.serve("_site") # or "public", "build", "dist"
15
+ ```ruby
16
+ require 'capybara_screenshot_diff/static'
17
+ CapybaraScreenshotDiff.serve("_site")
10
18
  ```
19
+ </details>
11
20
 
12
21
  This sets up Capybara to serve static files and configures screenshot paths automatically.
13
22
 
@@ -24,7 +33,8 @@ Only commit the baseline screenshots (e.g., `homepage.png`). The `.base.png`, `.
24
33
  Add to your test helper:
25
34
 
26
35
  ```ruby
27
- require 'capybara_screenshot_diff/reporters/html'
36
+ require 'snap_diff/reporters/html' # canonical
37
+ # require 'capybara_screenshot_diff/reporters/html' # legacy, same thing
28
38
  ```
29
39
 
30
40
  ### 2. Reusable composite action (recommended)
@@ -2,10 +2,25 @@
2
2
 
3
3
  ## Quick Setup
4
4
 
5
- Configure all settings in one place using the `configure` helper:
5
+ **Canonical (v2):** every setting lives on one flat object, `SnapDiff.config`.
6
6
 
7
7
  ```ruby
8
8
  # In test_helper.rb or rails_helper.rb
9
+ SnapDiff.configure do |config|
10
+ config.window_size = [1280, 1024]
11
+ config.stability_time_limit = 1
12
+ config.blur_active_element = true
13
+ config.hide_caret = true
14
+ config.driver = :vips
15
+ config.tolerance = 0.0005
16
+ config.color_distance_limit = 15
17
+ end
18
+ ```
19
+
20
+ **Legacy (still supported):** the two-holder block, split across `Capybara::Screenshot` and
21
+ `Capybara::Screenshot::Diff`.
22
+
23
+ ```ruby
9
24
  Capybara::Screenshot::Diff.configure do |screenshot, diff|
10
25
  screenshot.window_size = [1280, 1024]
11
26
  screenshot.stability_time_limit = 1
@@ -17,6 +32,20 @@ Capybara::Screenshot::Diff.configure do |screenshot, diff|
17
32
  end
18
33
  ```
19
34
 
35
+ `SnapDiff::Config` **is** the storage; the legacy accessors are thin delegators onto it. There is
36
+ one source of truth, so a write through either surface is visible through the other — mixing them
37
+ is safe, and you can migrate a suite one line at a time:
38
+
39
+ ```ruby
40
+ SnapDiff.config.window_size = [1280, 1024]
41
+ Capybara::Screenshot.window_size # => [1280, 1024]
42
+ ```
43
+
44
+ Every option name below is identical on both surfaces — only the receiver changes. The one
45
+ exception: `Capybara::Screenshot.enabled` is `SnapDiff.config.screenshot_enabled`, because
46
+ `SnapDiff.config.enabled` is taken by `Capybara::Screenshot::Diff.enabled`. See
47
+ [SnapDiff — the canonical API](snapdiff.md) for the full SnapDiff-native surface.
48
+
20
49
  **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.
21
50
 
22
51
  **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.
@@ -77,7 +106,7 @@ screenshot 'dashboard', color_distance_limit: 15
77
106
 
78
107
  **Tier 1 — Zero config (works immediately):**
79
108
  `blur_active_element`, `hide_caret`, and `fail_if_new` (in CI) are enabled by default.
80
- Just `require 'capybara_screenshot_diff/minitest'` and call `screenshot`.
109
+ Just `require 'snap_diff/integrations/minitest'` (legacy: `capybara_screenshot_diff/minitest`) and call `screenshot`.
81
110
 
82
111
  **Tier 2 — Set when tests are flaky:**
83
112
 
data/docs/drivers.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # Image Processing Drivers
2
2
 
3
+ > **Canonical equivalents.** Global settings shown here as
4
+ > `Capybara::Screenshot::Diff.<option> = …` are also `SnapDiff.config.<option> = …` — same
5
+ > option names, same storage, either surface works. Writing your own driver? See
6
+ > [Custom drivers](snapdiff.md#custom-drivers) for the `SnapDiff::Driver` mixin and how
7
+ > registration in `SnapDiff::Drivers.loaded` works.
8
+
3
9
  ## Perceptual color comparison (VIPS only)
4
10
 
5
11
  By default, color differences are measured using raw RGB channel distance. This can produce
@@ -1,5 +1,19 @@
1
1
  # Framework Setup
2
2
 
3
+ > **Canonical equivalents.** This page uses the legacy `CapybaraScreenshotDiff` names, which keep
4
+ > working. Each has a `SnapDiff` home:
5
+ >
6
+ > | This page | Canonical |
7
+ > |-----------|-----------|
8
+ > | `require "capybara_screenshot_diff/minitest"` | `require "snap_diff/integrations/minitest"` |
9
+ > | `require "capybara_screenshot_diff/rspec"` | `require "snap_diff/integrations/rspec"` |
10
+ > | `require "capybara_screenshot_diff/cucumber"` | `require "snap_diff/integrations/cucumber"` |
11
+ > | `CapybaraScreenshotDiff::DSL` | `SnapDiff::DSL` |
12
+ > | `CapybaraScreenshotDiff::Minitest::Assertions` | `SnapDiff::Minitest::Assertions` |
13
+ > | `CapybaraScreenshotDiff.finalize_reporters!` | `SnapDiff::Reporting.finalize!` |
14
+ >
15
+ > The canonical setup, written out in full, is in [SnapDiff — the canonical API](snapdiff.md).
16
+
3
17
  ## Including DSL
4
18
 
5
19
  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.
@@ -76,12 +90,16 @@ end
76
90
 
77
91
  ## Custom Test Frameworks
78
92
 
79
- Minitest, RSpec, and Cucumber are supported out of the box. For other frameworks, call `finalize_reporters!` in your framework's "after suite" hook:
93
+ Minitest, RSpec, and Cucumber are supported out of the box. For other frameworks, call the
94
+ end-of-suite hook yourself:
80
95
 
81
96
  ```ruby
82
- CapybaraScreenshotDiff.finalize_reporters!
97
+ SnapDiff::Reporting.finalize! # canonical
98
+ CapybaraScreenshotDiff.finalize_reporters! # legacy, same thing
83
99
  ```
84
100
 
85
- This generates the HTML report and prints the summary.
101
+ This generates the HTML report and prints the summary. A framework also needs the per-test
102
+ lifecycle wired up — see
103
+ [Frameworks other than Minitest/RSpec/Cucumber](snapdiff.md#frameworks-other-than-minitestrspeccucumber).
86
104
 
87
105
  [← Back to README](../README.md)
data/docs/reporters.md CHANGED
@@ -6,7 +6,8 @@ Generate an interactive Web UI report of screenshot differences:
6
6
 
7
7
  ```ruby
8
8
  # Add to test_helper.rb — one line, that's it
9
- require 'capybara_screenshot_diff/reporters/html'
9
+ require 'snap_diff/reporters/html' # canonical
10
+ # require 'capybara_screenshot_diff/reporters/html' # legacy, same thing
10
11
  ```
11
12
 
12
13
  After running tests, open the report (generated only when there are failures):
@@ -21,7 +22,7 @@ The report includes a sidebar with thumbnails, side-by-side comparison with diff
21
22
 
22
23
  ## Custom Reporters
23
24
 
24
- Build your own reporter by implementing `record` and `finalize`:
25
+ Build your own reporter by implementing `record`, `finalize` and `summary`:
25
26
 
26
27
  ```ruby
27
28
  class MyReporter
@@ -33,14 +34,28 @@ class MyReporter
33
34
  end
34
35
 
35
36
  def finalize
36
- # called once at process exit — write summary, upload report, etc.
37
+ # called once at end of suite — write summary, upload report, etc.
38
+ end
39
+
40
+ def summary
41
+ # printed to stdout after finalize; return nil to print nothing
42
+ nil
37
43
  end
38
44
  end
39
45
 
40
46
  # Register in test_helper.rb
41
- CapybaraScreenshotDiff.reporters << MyReporter.new
47
+ SnapDiff::Reporting.register(MyReporter.new) # canonical — appends under the mutex
48
+ # CapybaraScreenshotDiff.reporters << MyReporter.new # legacy, same list, skips the lock
42
49
  ```
43
50
 
44
- Reporters are notified before assertions are cleared on each test teardown. `finalize` is called via `at_exit`.
51
+ Reporters are notified before assertions are cleared on each test teardown. `finalize` runs from
52
+ the framework's end-of-suite hook (`Minitest.after_run`, RSpec `after(:suite)`, Cucumber
53
+ `AfterAll`), which calls `SnapDiff::Reporting.finalize!`.
54
+
55
+ **Do implement `summary`.** `finalize!` calls it unconditionally, so a reporter without it is
56
+ finalized and then warned about. A reporter that raises is warned about and skipped — the others
57
+ still run.
58
+
59
+ Full details in [Custom reporters](snapdiff.md#custom-reporters).
45
60
 
46
61
  [← Back to README](../README.md)
data/docs/snapdiff.md ADDED
@@ -0,0 +1,326 @@
1
+ # SnapDiff — the canonical API
2
+
3
+ Everything in this gem lives under `SnapDiff` since v2. This page is the SnapDiff-native
4
+ reference: setup, configuration, the object map, and the extension points — all using canonical
5
+ names only.
6
+
7
+ The legacy `Capybara::Screenshot::Diff` / `CapybaraScreenshotDiff` names still work (they resolve
8
+ to the same objects, with a one-time deprecation warning per constant), and the rest of the docs
9
+ still teach them. Nothing here replaces a working setup — it is what you write for **new** code.
10
+ For migrating an existing suite, see [UPGRADING.md](UPGRADING.md).
11
+
12
+ ## Quick start
13
+
14
+ ### Minitest
15
+
16
+ ```ruby
17
+ # test/test_helper.rb
18
+ require "snap_diff/integrations/minitest"
19
+ ```
20
+
21
+ ```ruby
22
+ # test/application_system_test_case.rb
23
+ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
24
+ include SnapDiff::Minitest::Assertions # brings in SnapDiff::DSL too
25
+ end
26
+ ```
27
+
28
+ ```ruby
29
+ class HomepageTest < ApplicationSystemTestCase
30
+ test "homepage" do
31
+ visit "/"
32
+ assert_matches_screenshot "homepage"
33
+ end
34
+ end
35
+ ```
36
+
37
+ `SnapDiff::Minitest::Assertions` already includes `SnapDiff::DSL`, so a separate
38
+ `include SnapDiff::DSL` is not needed (it is harmless if you have it).
39
+
40
+ > **The require path is `snap_diff/integrations/…`, not `snap_diff/…`.**
41
+ > `require "snap_diff/minitest"` raises `LoadError` — there is no such file. The integrations
42
+ > live one level down, mirroring `lib/snap_diff/integrations/`.
43
+
44
+ ### RSpec
45
+
46
+ ```ruby
47
+ # spec/rails_helper.rb
48
+ require "snap_diff/integrations/rspec"
49
+ ```
50
+
51
+ This registers the `match_screenshot` matcher and includes `SnapDiff::DSL` into
52
+ `type: :feature` and `type: :system` examples automatically:
53
+
54
+ ```ruby
55
+ RSpec.describe "Homepage", type: :system do
56
+ it "looks right" do
57
+ visit "/"
58
+ expect(page).to match_screenshot("homepage")
59
+ end
60
+ end
61
+ ```
62
+
63
+ For other example types, include the DSL yourself:
64
+
65
+ ```ruby
66
+ RSpec.describe "Admin", type: :request do
67
+ include SnapDiff::DSL
68
+ end
69
+ ```
70
+
71
+ ### Cucumber
72
+
73
+ ```ruby
74
+ # features/support/env.rb
75
+ require "snap_diff/integrations/cucumber"
76
+ ```
77
+
78
+ The DSL is added to the Cucumber `World`, so steps can call `screenshot` /
79
+ `assert_matches_screenshot` directly. This file must be loaded from inside a Cucumber run — it
80
+ calls `World`, `Before`, `After` and `AfterAll` at load time and raises `NoMethodError` if
81
+ required outside one.
82
+
83
+ ### Static sites (Hugo, Jekyll, plain HTML)
84
+
85
+ ```ruby
86
+ require "snap_diff/static"
87
+
88
+ SnapDiff.serve("_site") # or "public", "build", "dist"
89
+ SnapDiff.serve("_site", root: Dir.pwd) # root defaults to Dir.pwd
90
+ ```
91
+
92
+ `SnapDiff.serve` points Capybara at the built directory and sets the screenshot root. It also
93
+ loads the Minitest integration. See
94
+ [CI & Non-Rails Integration](ci-integration.md#non-rails-projects-hugo-jekyll-static-sites).
95
+
96
+ ## Configuration
97
+
98
+ All 27 settings live on one flat object, `SnapDiff.config` (a `SnapDiff::Config`).
99
+
100
+ ```ruby
101
+ # test_helper.rb / rails_helper.rb
102
+ SnapDiff.configure do |config|
103
+ config.window_size = [1280, 1024]
104
+ config.tolerance = 0.0005
105
+ config.driver = :vips
106
+ config.save_path = "doc/screenshots"
107
+ end
108
+
109
+ # Or set them one at a time
110
+ SnapDiff.config.hide_caret = true
111
+ SnapDiff.config.tolerance # => 0.0005
112
+ ```
113
+
114
+ `SnapDiff::Config` **is** the storage. The legacy `Capybara::Screenshot.*` and
115
+ `Capybara::Screenshot::Diff.*` accessors are thin delegators onto it — one storage, two views —
116
+ so a write through either surface is immediately visible through the other:
117
+
118
+ ```ruby
119
+ SnapDiff.config.window_size = [1280, 1024]
120
+ Capybara::Screenshot.window_size # => [1280, 1024]
121
+ ```
122
+
123
+ `SnapDiff.start` is the same call shape as the old `Capybara::Screenshot::Diff.configure`, if
124
+ you prefer the two-holder form:
125
+
126
+ ```ruby
127
+ SnapDiff.start do |screenshot, diff|
128
+ screenshot.window_size = [1280, 1024]
129
+ diff.tolerance = 0.0005
130
+ end
131
+ ```
132
+
133
+ Three derived, read-only values are computed from the settings above:
134
+
135
+ | Method | What it returns |
136
+ |--------|-----------------|
137
+ | `SnapDiff.config.active?` | Whether screenshots are taken at all (ex `Capybara::Screenshot.active?`) |
138
+ | `SnapDiff.config.screenshot_area` | `save_path`, optionally segmented per OS and per Capybara driver |
139
+ | `SnapDiff.config.default_options` | The capture/compare defaults handed to `SnapDiff::Comparison` |
140
+
141
+ Every option's meaning is documented in the
142
+ [Configuration Reference](configuration.md) — the names are identical, only the receiver differs.
143
+ The one rename: `Capybara::Screenshot.enabled` is `SnapDiff.config.screenshot_enabled`, because
144
+ `SnapDiff.config.enabled` is taken by `Capybara::Screenshot::Diff.enabled`.
145
+
146
+ ## Object map
147
+
148
+ `require "snap_diff"` gives you the compare/configure core. The test-suite pieces come with the
149
+ integration require; a few objects need their own require, noted below.
150
+
151
+ | Object | What it is for |
152
+ |--------|----------------|
153
+ | `SnapDiff.config`, `SnapDiff::Config` | Every setting, one flat object. The storage. |
154
+ | `SnapDiff.configure`, `SnapDiff.start` | Config block helpers (consolidated / v1 shape) |
155
+ | `SnapDiff.compare` | Compare two image files directly, no browser |
156
+ | `SnapDiff::Comparison` | The layered comparison engine (ex `ImageCompare`) |
157
+ | `SnapDiff::Comparison::Images` | Frozen bundle a comparison operates on: both images, their paths, the driver and the options |
158
+ | `SnapDiff::ComparisonResult` | Result value object: `different?`, region, metadata (ex `Difference`) |
159
+ | `SnapDiff::Region` | Bounding box value object — `from_edge_coordinates`, `to_edge_coordinates` |
160
+ | `SnapDiff::DSL` | `screenshot`, `assert_matches_screenshot`, `capture_screenshot`, groups/sections |
161
+ | `SnapDiff::Minitest::Assertions` | Minitest wiring (`snap_diff/integrations/minitest`) |
162
+ | `SnapDiff::Error` | Base class for every error this gem raises |
163
+ | `SnapDiff::ExpectationNotMet` | A screenshot did not match its baseline |
164
+ | `SnapDiff::UnstableImage` | No stable capture within `stability_time_limit` / `wait` |
165
+ | `SnapDiff::WindowSizeMismatchError` | Browser window is not the configured `window_size` |
166
+ | `SnapDiff::Driver` | Mixin with the shared driver defaults (`require "snap_diff/driver"`) |
167
+ | `SnapDiff::Drivers` | Driver factory and registry — `.for`, `.loaded`, `.available` |
168
+ | `SnapDiff::Reporting` | Process-global reporter lifecycle (`require "snap_diff/reporting"`) |
169
+ | `SnapDiff::Reporters::HTML` | The interactive HTML report (`require "snap_diff/reporters/html"`) |
170
+ | `SnapDiff::Reporters::Default` | Builds the annotated diff images and the failure message |
171
+ | `SnapDiff.session` | The per-test assertion registry (fiber-local) |
172
+ | `SnapDiff.reset` | Ends a test: notifies reporters, clears the session |
173
+ | `SnapDiff.pending_screenshots_message` | Skip message when a new screenshot has no baseline |
174
+ | `SnapDiff::Capture::Viewport` | Per-capture viewport check seam (`require "snap_diff/capture/viewport"`) |
175
+ | `SnapDiff.serve` | Point Capybara at a static site directory (`require "snap_diff/static"`) |
176
+
177
+ ## Compare two images without a browser
178
+
179
+ Works on anything on disk — rendered PDFs, generated charts, CI artifacts:
180
+
181
+ ```ruby
182
+ require "snap_diff"
183
+
184
+ result = SnapDiff.compare("baseline.png", "current.png")
185
+ result.quick_equal? # => true when byte-identical / pixel-identical
186
+ result.different? # => true when the difference exceeds the configured thresholds
187
+ result.difference # => SnapDiff::ComparisonResult with region and metadata
188
+
189
+ # Per-call option overrides, merged over SnapDiff.config.default_options
190
+ SnapDiff.compare("baseline.png", "current.png", tolerance: 0.5)
191
+ ```
192
+
193
+ Note the argument order: baseline first, current second ("compare baseline against current").
194
+
195
+ ## Custom reporters
196
+
197
+ A reporter is any object answering `record(assertions)`, `finalize`, and `summary`. Register it
198
+ once, for the rest of the process:
199
+
200
+ ```ruby
201
+ require "snap_diff/reporting"
202
+
203
+ class SlackReporter
204
+ def initialize = @failures = []
205
+
206
+ # Called once per finished test, with that test's assertions.
207
+ def record(assertions)
208
+ assertions.each do |assertion|
209
+ next unless assertion.compare&.difference&.different?
210
+ @failures << assertion.name
211
+ end
212
+ end
213
+
214
+ # Called once at end of suite.
215
+ def finalize
216
+ post_to_slack(@failures) unless @failures.empty?
217
+ end
218
+
219
+ # Printed to stdout after finalize. Return nil to print nothing.
220
+ def summary
221
+ @failures.empty? ? nil : "#{@failures.size} screenshot(s) changed"
222
+ end
223
+ end
224
+
225
+ SnapDiff::Reporting.register(SlackReporter.new)
226
+ ```
227
+
228
+ `register` appends under a mutex, so concurrent registrations cannot lose one — prefer it over
229
+ mutating `SnapDiff::Reporting.reporters` directly.
230
+
231
+ Do implement `summary`. `finalize!` calls it unconditionally; a reporter without it is finalized
232
+ and then warned about (`[snap_diff] Reporter … failed (NoMethodError: undefined method 'summary')`).
233
+
234
+ A reporter that raises is warned about and skipped — the other reporters still run.
235
+
236
+ The bundled HTML report is just a pre-registered reporter of this kind:
237
+
238
+ ```ruby
239
+ require "snap_diff/reporters/html" # registers SnapDiff::Reporters::HTML itself
240
+ ```
241
+
242
+ ### Frameworks other than Minitest/RSpec/Cucumber
243
+
244
+ The three bundled integrations call the lifecycle for you. Wiring another framework means calling
245
+ three things:
246
+
247
+ ```ruby
248
+ require "snap_diff/dsl" # SnapDiff::DSL, BrowserHelpers, the session accessors
249
+ require "snap_diff/reporting" # SnapDiff::Reporting
250
+
251
+ # before each test
252
+ SnapDiff::BrowserHelpers.resize_window_if_needed
253
+
254
+ # after each test
255
+ SnapDiff.session.verify # raises SnapDiff::ExpectationNotMet on a mismatch
256
+ msg = SnapDiff.pending_screenshots_message # non-nil => skip the test with this message
257
+ SnapDiff.reset # always: notifies reporters, clears the session
258
+
259
+ # after the suite
260
+ SnapDiff::Reporting.finalize!
261
+ ```
262
+
263
+ ## Custom drivers
264
+
265
+ A driver is a plain object that does the image work. Include `SnapDiff::Driver` for the shared
266
+ defaults, then implement the operations the comparison engine calls:
267
+
268
+ ```ruby
269
+ require "snap_diff/driver"
270
+
271
+ class MyDriver
272
+ include SnapDiff::Driver
273
+
274
+ # Provided by the mixin, override only if your image objects differ:
275
+ # width_for(image), height_for(image), dimension(image),
276
+ # image_area_size(image), same_dimension?(comparison), supports?(feature)
277
+
278
+ # Implement (this is what both bundled drivers implement):
279
+ # from_file(path), load_images(base_path, new_path), save_image_to(image, path)
280
+ # same_pixels?(comparison), find_difference_region(comparison)
281
+ # crop(region, image), resize_image_to(image, w, h)
282
+ # add_black_box(image, region), draw_rectangles(images, region, ...)
283
+ end
284
+ ```
285
+
286
+ `supports?(feature)` is just `respond_to?(feature)` — the engine uses it to skip optional
287
+ operations. The VIPS driver additionally implements `filter_image_with_median`, `merge`,
288
+ `highlight_mask` and `difference_level`; ChunkyPNG does not, and `supports?` is how that is
289
+ detected. Look at `lib/snap_diff/drivers/chunky_png_driver.rb` for the smaller of the two
290
+ reference implementations.
291
+
292
+ ### Registration
293
+
294
+ `SnapDiff::Drivers.loaded` is the registry: a mutable `name => driver class` hash. Register by
295
+ writing into it, then select the driver by that name:
296
+
297
+ ```ruby
298
+ SnapDiff::Drivers.loaded[:my_driver] = MyDriver
299
+
300
+ SnapDiff.config.driver = :my_driver # globally
301
+ screenshot "index", driver: :my_driver # or per screenshot
302
+ ```
303
+
304
+ Resolution goes through `SnapDiff::Drivers.for`, which looks the symbol up in `loaded` and calls
305
+ `.new` on the class — **your driver class must be instantiable with no arguments**. A pre-built
306
+ instance skips the registry entirely:
307
+
308
+ ```ruby
309
+ screenshot "index", driver: MyDriver.new # any non-Symbol is used as-is
310
+ ```
311
+
312
+ `SnapDiff::Drivers.available` is the *detected* list (`[:vips, :chunky_png]`, filled at load time
313
+ by probing for the `ruby-vips` and `chunky_png` gems). It is a read-only view of detection, not
314
+ the registry — registering a custom driver does not add it there, and `driver: :auto` picks
315
+ `available.first`. Custom drivers must always be named explicitly.
316
+
317
+ ## Related
318
+
319
+ - [Framework Setup](framework-setup.md) — the same three integrations under their legacy names
320
+ - [Configuration Reference](configuration.md) — what every option does
321
+ - [Image Processing Drivers](drivers.md) — VIPS vs ChunkyPNG, perceptual threshold
322
+ - [Web UI & Custom Reporters](reporters.md) — the HTML report in detail
323
+ - [Architecture](architecture.md) — how the pieces fit together internally
324
+ - [UPGRADING.md](UPGRADING.md) — migrating an existing suite off the legacy names
325
+
326
+ [← Back to README](../README.md)
@@ -100,9 +100,10 @@ Do not:
100
100
 
101
101
  Runtime state is thread-local (above), but *loading* the gem is a separate
102
102
  concern. The require graph is deliberately acyclic: `lib/snap_diff/*` units
103
- depend only on the legacy-config leaf
104
- (`capybara/screenshot/diff/config_legacy`) and specific sibling units, the
105
- umbrella files depend on the units, and nothing requires back up the chain.
103
+ depend only on the config-storage leaf (`snap_diff/config`, which the legacy
104
+ view `capybara/screenshot/diff/config_legacy` requires) and specific sibling
105
+ units, the umbrella files depend on the units, and nothing requires back up
106
+ the chain.
106
107
 
107
108
  Eager mutual requires between entry points are forbidden, even guarded ones:
108
109
  per-thread "loading" flags cannot serialize Ruby's process-global per-file
@@ -1,80 +1,44 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Legacy Capybara::Screenshot / Capybara::Screenshot::Diff mattr_accessor
4
- # config module, extracted out of capybara_screenshot_diff.rb so it is a
5
- # leaf: it can be required by both capybara_screenshot_diff.rb (the old
6
- # umbrella entry point) and snap_diff.rb (the canonical entry point)
7
- # without either of *those* requiring the other back.
3
+ # Legacy Capybara::Screenshot / Capybara::Screenshot::Diff config surface.
8
4
  #
9
- # Why this file has to exist at all: several lib/snap_diff/* units
10
- # (Screenshoter, SnapManager, Utils) are referenced here at class-body
11
- # eval time (mattr_accessor default blocks, AVAILABLE_DRIVERS) rather
12
- # than at call time, so they must be real, already-loaded classes before
13
- # this module body runs. Since v2 step 6 they are pulled in and referenced
14
- # under their canonical SnapDiff names: the old-name aliases are lazy
15
- # const_missing shims that emit deprecation warnings, and the gem's own
16
- # code must stay warning-free. Everything else this module references
17
- # (Os, Comparison) is referenced only inside method bodies, resolved
18
- # lazily at call time by whichever entry point loaded them.
19
- require "snap_diff/screenshoter"
20
- require "snap_diff/snap_manager"
5
+ # Since ADR-008 step 1 the storage lives in SnapDiff::Config -- the require
6
+ # leaf of the config graph (see its own header) -- and since step 7b the
7
+ # DERIVED values (active?, screenshot_area, default_options) live there
8
+ # too. snap_diff/config.rb also generates the old accessor names as thin
9
+ # delegators from SnapDiff::Config::MAPPING, so nothing but forwarders is
10
+ # left here. The v1 surface (Capybara::Screenshot.window_size = ...,
11
+ # Diff.configure { ... }, Diff.compare) keeps working unchanged: one
12
+ # storage, two views.
13
+ #
14
+ # Load order: requiring snap_diff/config first also eagerly evaluates the
15
+ # require-time defaults (ENV["CI"] for fail_if_new, Rails.root/pwd for
16
+ # root) at this same load moment, exactly when the old mattr_accessor
17
+ # default blocks used to run. snap_diff/config never requires back here,
18
+ # so the graph stays acyclic.
19
+ require "snap_diff/config"
20
+ # AVAILABLE_DRIVERS below is evaluated at class-body eval time, so Utils
21
+ # must be a real, already-loaded module before this module body runs.
21
22
  require "snap_diff/utils"
22
23
 
23
24
  module Capybara
24
25
  module Screenshot
25
- mattr_accessor :add_driver_path
26
- mattr_accessor :add_os_path
27
- mattr_accessor(:blur_active_element) { true }
28
- mattr_accessor :enabled
29
- mattr_accessor(:hide_caret) { true }
30
- mattr_accessor :disable_animations
31
- mattr_reader(:root) { (defined?(Rails) && defined?(Rails.root) && Rails.root) || Pathname(".").expand_path }
32
- mattr_accessor :stability_time_limit
33
- mattr_accessor :window_size
34
- mattr_accessor(:save_path) { "doc/screenshots" }
35
- mattr_accessor(:use_lfs)
36
- mattr_accessor(:screenshot_format) { "png" }
37
- mattr_accessor(:capybara_screenshot_options) { {} }
38
-
39
26
  class << self
40
- def root=(path)
41
- @@root = Pathname(path).expand_path
42
- end
43
-
44
27
  def active?
45
- enabled || (enabled.nil? && Diff.enabled)
28
+ SnapDiff.config.active?
46
29
  end
47
30
 
48
31
  def screenshot_area
49
- parts = [Screenshot.save_path]
50
- parts << SnapDiff::Os.name if Screenshot.add_os_path
51
- parts << Capybara.current_driver.to_s if Screenshot.add_driver_path
52
- File.join(*parts)
32
+ SnapDiff.config.screenshot_area
53
33
  end
54
34
 
55
35
  def screenshot_area_abs
56
- root / screenshot_area
36
+ SnapDiff.config.screenshot_area_abs
57
37
  end
58
38
  end
59
39
 
60
40
  # Module to track screenshot changes
61
41
  module Diff
62
- mattr_accessor(:delayed) { true }
63
- mattr_accessor :area_size_limit
64
- mattr_accessor(:fail_if_new) { !ENV["CI"].nil? && !ENV["CI"].empty? }
65
- mattr_accessor(:pending_if_new) { false }
66
- mattr_accessor(:fail_on_difference) { true }
67
- mattr_accessor :color_distance_limit
68
- mattr_accessor(:enabled) { true }
69
- mattr_accessor :shift_distance_limit
70
- mattr_accessor :skip_area
71
- mattr_accessor(:driver) { :auto }
72
- mattr_accessor :tolerance
73
- mattr_accessor :perceptual_threshold
74
-
75
- mattr_accessor(:screenshoter) { SnapDiff::Screenshoter }
76
- mattr_accessor(:manager) { SnapDiff::SnapManager }
77
-
78
42
  AVAILABLE_DRIVERS = SnapDiff::Utils.detect_available_drivers.freeze
79
43
 
80
44
  # Configure screenshot and diff settings in one block.
@@ -85,28 +49,18 @@ module Capybara
85
49
  # diff.driver = :vips
86
50
  # diff.tolerance = 0.0005
87
51
  # end
52
+ # The bare `yield` (rather than an explicit &block) keeps this
53
+ # method's published arity byte-identical to what it always had.
88
54
  def self.configure
89
- yield Screenshot, self
55
+ SnapDiff.start { |screenshot, diff| yield screenshot, diff }
90
56
  end
91
57
 
92
58
  def self.compare(baseline_path, current_path, **options)
93
- SnapDiff::Comparison.new(current_path, baseline_path, default_options.merge(options))
59
+ SnapDiff.compare(baseline_path, current_path, **options)
94
60
  end
95
61
 
96
62
  def self.default_options
97
- {
98
- area_size_limit: area_size_limit,
99
- color_distance_limit: color_distance_limit,
100
- driver: driver,
101
- screenshot_format: Screenshot.screenshot_format,
102
- capybara_screenshot_options: Screenshot.capybara_screenshot_options,
103
- perceptual_threshold: perceptual_threshold,
104
- shift_distance_limit: shift_distance_limit,
105
- skip_area: skip_area,
106
- stability_time_limit: Screenshot.stability_time_limit,
107
- tolerance: tolerance || ((driver == :vips) ? 0.001 : nil),
108
- wait: Capybara.default_max_wait_time
109
- }
63
+ SnapDiff.config.default_options
110
64
  end
111
65
  end
112
66
  end