capybara-screenshot-diff 1.13.0 → 1.14.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5d2139e98f2c24b265a13882b8faf30282f5ef519dcbb77907f5d69d8f565606
4
- data.tar.gz: 9302c796f95734c4ec4383f926c0aad03630caed8505f1d8fc815fe11bdb3c61
3
+ metadata.gz: dba172cde687794eeedbffe4c4112d58ed5c4e2fcac5cdaf93b84827396e832d
4
+ data.tar.gz: a6b45b9bfc4ff349b506c7045f54392101c1bbfecda7b51475758fd2ee1536c0
5
5
  SHA512:
6
- metadata.gz: b072316d36fe69af44d3f318ba2d24968e9e0b377f85842c9a21d29aeb4ae7375a6b5b02c4842c9f3d9eb3115ed4fbcc33edff6c3b74b8ce788069d2301da95a
7
- data.tar.gz: 30389f03f593e74c888a2d9de77717f1c839ad8ce41539ef0e6633b5dcff609ed546d7ed5279b3ab7ac9c86c53ed6608872e5b28a70663c7c2aef7c0974fcdf5
6
+ metadata.gz: afc332553b0bdafe8a47d82376b83b866bcd25144132f6e6ba481e41b03fd113ea74cf25f1decfcff8397664b1d4e2469abbfd21bebfde2a2c42f332ca0220dc
7
+ data.tar.gz: 1bd843fa71d17a0eac95bf1674a8d76352a8fe8c93c581840f9a50f63106edd40c5964b25ac36add805c6baf4129138e01aca5f73df30b34646fb1c2814adacd
data/CHANGELOG.md CHANGED
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [v1.14.0] - 2026-08-22
9
+
10
+ ### Added
11
+ - **`SnapDiff` namespace aliases** — `SnapDiff::Comparison`, `SnapDiff.compare`, and `SnapDiff.start` provide a forward-looking entry point onto the existing `Capybara::Screenshot::Diff` API, no behavior changes ([#166](https://github.com/snap-diff/snap_diff-capybara/issues/166))
12
+
13
+ ### Fixed
14
+ - **Standalone require path** — `require "capybara_screenshot_diff"` (or `require "snap_diff"`) followed by `Diff.compare` no longer raises `NameError` for the missing drivers require ([#194](https://github.com/snap-diff/snap_diff-capybara/pull/194))
15
+
16
+ ### Internal
17
+ - Merged `DifferenceFinder` into `ImageCompare` (ADR-004 step) ([#192](https://github.com/snap-diff/snap_diff-capybara/pull/192))
18
+ - Extracted `AnnotationService` from `Reporters::Default`; full public surface preserved via delegation ([#193](https://github.com/snap-diff/snap_diff-capybara/pull/193))
19
+ - Consolidated `pending_if_new` teardown logic into one shared helper across Minitest/RSpec/Cucumber adapters ([#197](https://github.com/snap-diff/snap_diff-capybara/pull/197))
20
+ - Test-suite driver-coverage banner and CI guard against silently missing drivers ([#198](https://github.com/snap-diff/snap_diff-capybara/pull/198))
21
+
22
+ ---
23
+
8
24
  ## [v1.13.0] - 2026-08-22
9
25
 
10
26
  ### Added
data/docs/RELEASE_PREP.md CHANGED
@@ -1,24 +1,23 @@
1
- # Release Preparation — v1.13.0
1
+ # Release Preparation — v1.14.0
2
2
 
3
3
  ## Summary
4
4
 
5
- Issue #191 API decoupling: `assert_matches_screenshot` becomes the primary
6
- assertion method, plus a capture-only API and opt-in pending-instead-of-pass
7
- for missing baselines.
5
+ SnapDiff namespace aliases (ADR-004 PR 9), standalone-require fix, and three
6
+ internal consolidation refactors (ADR-004 PR 2/PR 7, teardown logic).
8
7
 
9
8
  ## Release Checklist
10
9
 
11
10
  ### Pre-Release
12
11
 
13
- - [x] Update version to `1.13.0`
14
- - [x] Run tests: `bundle exec rake test:unit` (235 runs, 0 failures)
12
+ - [x] Update version to `1.14.0`
13
+ - [x] Run tests: `bundle exec rake test:unit` (green)
15
14
  - [x] Update CHANGELOG.md
16
15
 
17
16
  ### Release (One Click)
18
17
 
19
18
  1. Push to GitHub
20
19
  2. Go to [Actions → Release](https://github.com/snap-diff/snap_diff-capybara/actions/workflows/release.yml)
21
- 3. Click **Run workflow**, enter `1.13.0`
20
+ 3. Click **Run workflow**, enter `1.14.0`
22
21
  4. Workflow will: test → tag → publish to RubyGems → create GitHub Release
23
22
 
24
23
  ### Post-Release
@@ -28,15 +27,18 @@ for missing baselines.
28
27
 
29
28
  ## What Changed
30
29
 
31
- ### New Features
32
- - `capture_screenshot` DSL methodcapture without comparing or asserting
33
- - `compare:` option on `screenshot` `compare: false` captures only
34
- - `Capybara::Screenshot::Diff.pending_if_new` — mark tests skipped in teardown
35
- when a screenshot has no committed baseline (Minitest, RSpec, and Cucumber)
30
+ ### Added
31
+ - `SnapDiff` namespace aliases`SnapDiff::Comparison`, `SnapDiff.compare`,
32
+ `SnapDiff.start` (additive; deprecations deferred to v2.0)
36
33
 
37
- ### Behavior Changes
38
- - `assert_matches_screenshot` is now the primary assertion method; `screenshot`
39
- remains as a convenience wrapper and is safe to override in user test classes —
40
- the gem no longer calls it internally ([#191](https://github.com/snap-diff/snap_diff-capybara/issues/191))
34
+ ### Fixed
35
+ - Standalone `require "capybara_screenshot_diff"` + `Diff.compare` no longer
36
+ raises `NameError` (missing drivers require)
37
+
38
+ ### Internal
39
+ - `DifferenceFinder` merged into `ImageCompare`; `AnnotationService` extracted
40
+ from `Reporters::Default` (public surface preserved); `pending_if_new`
41
+ teardown consolidated across the three framework adapters; driver-coverage
42
+ banner + CI guard in the test suite
41
43
 
42
44
  See [CHANGELOG.md](../CHANGELOG.md) for full details.
data/docs/UPGRADING.md CHANGED
@@ -1,6 +1,91 @@
1
- # Upgrading to v1.12.0
1
+ # Upgrading
2
2
 
3
- ## Overview
3
+ ## Upgrading to v1.13.0
4
+
5
+ ### Overview
6
+
7
+ Version 1.13.0 is a **minor release** clarifying API terminology and adding new capture methods. No breaking changes — your existing code continues to work.
8
+
9
+ **Estimated upgrade time:** 0 minutes (no action required for most users)
10
+
11
+ ---
12
+
13
+ ### Quick Upgrade Path (Most Users)
14
+
15
+ ```ruby
16
+ # In your Gemfile
17
+ gem 'capybara-screenshot-diff', '~> 1.13.0'
18
+ ```
19
+
20
+ ```bash
21
+ bundle update capybara-screenshot-diff
22
+ bundle exec rake test
23
+ ```
24
+
25
+ **That's it!** Existing `screenshot` calls work unchanged. New methods available if needed.
26
+
27
+ ---
28
+
29
+ ### What Changed
30
+
31
+ #### API Clarification: Primary Method is `assert_matches_screenshot`
32
+
33
+ **v1.12.0 and earlier:** `screenshot` was the primary method
34
+ **v1.13.0+:** `assert_matches_screenshot` is the primary method
35
+
36
+ **Action required:** None. `screenshot` continues to work as-is.
37
+
38
+ The method names now better reflect their behavior:
39
+ - `assert_matches_screenshot(name)` — takes screenshot and asserts it matches baseline
40
+ - `screenshot(name, compare: true)` — convenience wrapper (same behavior as above when `compare: true`)
41
+ - `capture_screenshot(name)` — new: captures without asserting
42
+
43
+ ```ruby
44
+ # All three work and are safe to use:
45
+ assert_matches_screenshot "homepage" # Primary: explicit intent
46
+ screenshot "homepage" # Shorthand (familiar)
47
+ screenshot "homepage", compare: false # Capture only
48
+ capture_screenshot "homepage" # Also capture only
49
+ ```
50
+
51
+ **Safe to override:** You can safely define your own `screenshot` method in your test base class — the gem's implementation won't interfere.
52
+
53
+ ---
54
+
55
+ ### New: `capture_screenshot` Method
56
+
57
+ Capture without comparing to baseline:
58
+
59
+ ```ruby
60
+ capture_screenshot "dynamic_page" # No assertion
61
+ ```
62
+
63
+ Equivalent to: `screenshot "dynamic_page", compare: false`
64
+
65
+ ---
66
+
67
+ ### New: `Diff.pending_if_new` Helper
68
+
69
+ Mark baseline-less tests pending instead of failing during initial CI runs:
70
+
71
+ ```ruby
72
+ # In test_helper.rb — before running tests
73
+ Capybara::Screenshot::Diff.pending_if_new = true
74
+ ```
75
+
76
+ **CI requirement:** When using `pending_if_new`, ensure CI is configured with `fail_if_new: false` (see [Configuration Reference](configuration.md#quick-setup)):
77
+
78
+ ```ruby
79
+ Capybara::Screenshot::Diff.configure do |screenshot, diff|
80
+ diff.fail_if_new = false # Allow baselines to be added
81
+ end
82
+ ```
83
+
84
+ ---
85
+
86
+ ## Upgrading to v1.12.0
87
+
88
+ ### Overview
4
89
 
5
90
  Version 1.12.0 is a **minor release** with new features, performance improvements, and default behavior changes. This guide will help you upgrade smoothly.
6
91
 
data/docs/architecture.md CHANGED
@@ -100,7 +100,7 @@ The comparison engine uses a **layered optimization strategy** to balance speed
100
100
  - `quick_equal?` is designed for fast rejection — it early-returns as soon as a difference is found
101
101
  - `different?` triggers the full comparison if not already processed
102
102
  - `processed` guarantees the comparison is complete and returns the result with all metadata
103
- - The `DifferenceFinder` handles the actual pixel analysis, delegating to the driver
103
+ - `ImageCompare#analyze_difference` handles the actual pixel analysis, delegating to the driver
104
104
 
105
105
  ### 5. Drivers (`lib/capybara/screenshot/diff/drivers/`)
106
106
 
@@ -257,7 +257,6 @@ lib/
257
257
  image_compare.rb # Layered comparison engine
258
258
  comparison.rb # Comparison value object
259
259
  difference.rb # Difference result value object
260
- difference_finder.rb # Core diff analysis logic
261
260
  image_preprocessor.rb # Pre-processing (skip areas, median filter)
262
261
  area_calculator.rb # Crop/skip area coordinate resolution
263
262
  region.rb # Bounding box region value object
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Capybara::Screenshot::Diff
4
+ # Draws diff/skip-area rectangles and the heatmap overlay for a Difference,
5
+ # and saves the resulting images to their `.diff.*` / `.heatmap.diff.*` paths.
6
+ #
7
+ # Extracted from Reporters::Default (ADR-004 PR 7) so the reporter only
8
+ # builds error messages; this class owns all image annotation work.
9
+ class AnnotationService
10
+ attr_reader :annotated_image_path, :annotated_base_image_path, :heatmap_diff_path
11
+
12
+ def initialize(difference)
13
+ @difference = difference
14
+
15
+ comparison = difference.comparison
16
+ ext = comparison.new_image_path.extname.delete_prefix(".")
17
+ screenshot_format = comparison.options[:screenshot_format] || (ext unless ext.empty?) || "png"
18
+ @annotated_image_path = comparison.new_image_path.sub_ext(".diff.#{screenshot_format}")
19
+ @annotated_base_image_path = comparison.base_image_path.sub_ext(".diff.#{screenshot_format}")
20
+ @heatmap_diff_path = comparison.new_image_path.sub_ext(".heatmap.diff.#{screenshot_format}")
21
+ end
22
+
23
+ def clean_tmp_files
24
+ annotated_base_image_path.unlink if annotated_base_image_path.exist?
25
+ annotated_image_path.unlink if annotated_image_path.exist?
26
+ heatmap_diff_path.unlink if heatmap_diff_path.exist?
27
+ end
28
+
29
+ def annotate_and_save_images
30
+ save_annotation_for(new_image, annotated_image_path)
31
+ save_annotation_for(base_image, annotated_base_image_path)
32
+ save_heatmap_diff if difference.diff_mask
33
+ end
34
+
35
+ def save_annotation_for(image, image_path)
36
+ image = annotate_difference(image, difference.region)
37
+ image = annotate_skip_areas(image, difference.comparison.skip_area) if difference.comparison.skip_area
38
+
39
+ save(image, image_path.to_path)
40
+ end
41
+
42
+ def annotate_difference(image, region)
43
+ driver.draw_rectangles([image], region, CapybaraScreenshotDiff::RED_RGBA, offset: 1).first
44
+ end
45
+
46
+ def annotate_skip_areas(image, skip_areas)
47
+ skip_areas.reduce(image) do |memo, region|
48
+ driver.draw_rectangles([memo], region, CapybaraScreenshotDiff::ORANGE_RGBA).first
49
+ end
50
+ end
51
+
52
+ def save(image, image_path)
53
+ driver.save_image_to(image, image_path.to_s)
54
+ end
55
+
56
+ private
57
+
58
+ attr_reader :difference
59
+
60
+ def save_heatmap_diff
61
+ merged_image = driver.merge(new_image, base_image)
62
+ highlighted_mask = driver.highlight_mask(difference.diff_mask, merged_image, color: CapybaraScreenshotDiff::RED_RGBA)
63
+
64
+ save(highlighted_mask, heatmap_diff_path.to_path)
65
+ end
66
+
67
+ def base_image
68
+ difference.comparison.base_image
69
+ end
70
+
71
+ def new_image
72
+ difference.comparison.new_image
73
+ end
74
+
75
+ def driver
76
+ @_driver ||= difference.comparison.driver
77
+ end
78
+ end
79
+ end
@@ -4,8 +4,9 @@ require "pathname"
4
4
  require "fileutils"
5
5
 
6
6
  require "capybara/screenshot/diff/comparison"
7
+ require "capybara/screenshot/diff/difference"
8
+ require "capybara/screenshot/diff/drivers"
7
9
  require "capybara/screenshot/diff/image_preprocessor"
8
- require "capybara/screenshot/diff/difference_finder"
9
10
  require "capybara/screenshot/diff/reporters/default"
10
11
 
11
12
  module Capybara
@@ -36,6 +37,8 @@ module Capybara
36
37
  # - Only performing expensive operations when absolutely necessary
37
38
  # - Maintaining high accuracy for complex comparisons
38
39
  class ImageCompare
40
+ TOLERABLE_OPTIONS = [:tolerance, :color_distance_limit, :shift_distance_limit, :area_size_limit].freeze
41
+
39
42
  attr_reader :driver, :driver_options
40
43
  attr_reader :image_path, :base_image_path
41
44
  attr_reader :difference, :error_message
@@ -48,6 +51,7 @@ module Capybara
48
51
 
49
52
  @driver_options = options.freeze
50
53
  @driver = Drivers.for(@driver_options)
54
+ @without_tolerable_options = (driver_options.keys & TOLERABLE_OPTIONS).empty?
51
55
  end
52
56
 
53
57
  # Performs a quick comparison of two image files.
@@ -85,7 +89,7 @@ module Capybara
85
89
  # - `false` if the images are considered identical
86
90
  #
87
91
  # @see #processed
88
- # @see DifferenceFinder
92
+ # @see #analyze_difference
89
93
  def different?
90
94
  processed.difference.different?
91
95
  end
@@ -110,8 +114,8 @@ module Capybara
110
114
 
111
115
  private
112
116
 
113
- def difference_finder
114
- @difference_finder ||= DifferenceFinder.new(driver, driver_options)
117
+ def without_tolerable_options?
118
+ @without_tolerable_options
115
119
  end
116
120
 
117
121
  def load_images_and_build_comparison(base_path, new_path, options)
@@ -130,8 +134,41 @@ module Capybara
130
134
  # Create comparison with preprocessed images
131
135
  comparison = load_comparison(base_image_path, image_path, driver_options)
132
136
 
133
- # Use difference finder to analyze the comparison
134
- difference_finder.call(comparison, quick_mode: quick_mode)
137
+ analyze_difference(comparison, quick_mode: quick_mode)
138
+ end
139
+
140
+ # Analyzes the comparison and determines if images are different.
141
+ #
142
+ # @param comparison [Comparison] The comparison object containing images to analyze.
143
+ # @param quick_mode [Boolean] When true, performs minimal checks and returns early.
144
+ # In quick mode, returns [is_equal, difference] where:
145
+ # - is_equal is true if images are considered equal
146
+ # - difference is a Difference object or nil
147
+ # When false, returns a Difference object directly.
148
+ # @return [Array, Difference] Result format depends on quick_mode parameter.
149
+ def analyze_difference(comparison, quick_mode: true)
150
+ # Handle dimension differences
151
+ unless driver.same_dimension?(comparison)
152
+ result = Difference.build_null(comparison, comparison.base_image_path, comparison.new_image_path, {different_dimensions: true})
153
+ return quick_mode ? [false, result] : result
154
+ end
155
+
156
+ # Handle identical pixels
157
+ if driver.same_pixels?(comparison)
158
+ result = Difference.build_null(comparison, comparison.base_image_path, comparison.new_image_path)
159
+ return quick_mode ? [true, result] : result
160
+ end
161
+
162
+ # Handle early return for non-tolerable options
163
+ if quick_mode && without_tolerable_options?
164
+ return [false, nil]
165
+ end
166
+
167
+ # Process difference region
168
+ region = driver.find_difference_region(comparison)
169
+
170
+ # Only create a proper difference object if we've completed the comparison
171
+ quick_mode ? [!region.different?, region] : region
135
172
  end
136
173
 
137
174
  def difference=(new_difference)
@@ -1,18 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "capybara/screenshot/diff/annotation_service"
4
+
3
5
  module Capybara::Screenshot::Diff
4
6
  module Reporters
5
7
  class Default
6
- attr_reader :annotated_image_path, :annotated_base_image_path, :heatmap_diff_path, :difference
8
+ attr_reader :difference
7
9
 
8
10
  def initialize(difference)
9
11
  @difference = difference
12
+ @annotation_service = AnnotationService.new(difference)
13
+ end
14
+
15
+ def annotated_image_path
16
+ annotation_service.annotated_image_path
17
+ end
10
18
 
11
- ext = comparison.new_image_path.extname.delete_prefix(".")
12
- screenshot_format = difference.comparison.options[:screenshot_format] || (ext unless ext.empty?) || "png"
13
- @annotated_image_path = comparison.new_image_path.sub_ext(".diff.#{screenshot_format}")
14
- @annotated_base_image_path = comparison.base_image_path.sub_ext(".diff.#{screenshot_format}")
15
- @heatmap_diff_path = comparison.new_image_path.sub_ext(".heatmap.diff.#{screenshot_format}")
19
+ def annotated_base_image_path
20
+ annotation_service.annotated_base_image_path
21
+ end
22
+
23
+ def heatmap_diff_path
24
+ annotation_service.heatmap_diff_path
16
25
  end
17
26
 
18
27
  def generate
@@ -31,44 +40,35 @@ module Capybara::Screenshot::Diff
31
40
  end
32
41
 
33
42
  def clean_tmp_files
34
- annotated_base_image_path.unlink if annotated_base_image_path.exist?
35
- annotated_image_path.unlink if annotated_image_path.exist?
36
- heatmap_diff_path.unlink if heatmap_diff_path.exist?
37
- end
38
-
39
- def build_error_for_different_dimensions
40
- change_msg = [comparison.base_image, comparison.new_image]
41
- .map { |image| driver.dimension(image).join("x") }
42
- .join(" => ")
43
-
44
- "Dimensions have changed: #{change_msg}\n#{base_image_path.to_path}\n#{image_path.to_path}"
43
+ annotation_service.clean_tmp_files
45
44
  end
46
45
 
47
46
  def annotate_and_save_images
48
- save_annotation_for(new_image, annotated_image_path)
49
- save_annotation_for(base_image, annotated_base_image_path)
50
- save_heatmap_diff if difference.diff_mask
47
+ annotation_service.annotate_and_save_images
51
48
  end
52
49
 
53
50
  def save_annotation_for(image, image_path)
54
- image = annotate_difference(image, difference.region)
55
- image = annotate_skip_areas(image, difference.comparison.skip_area) if difference.comparison.skip_area
56
-
57
- save(image, image_path.to_path)
51
+ annotation_service.save_annotation_for(image, image_path)
58
52
  end
59
53
 
60
54
  def annotate_difference(image, region)
61
- driver.draw_rectangles([image], region, CapybaraScreenshotDiff::RED_RGBA, offset: 1).first
55
+ annotation_service.annotate_difference(image, region)
62
56
  end
63
57
 
64
58
  def annotate_skip_areas(image, skip_areas)
65
- skip_areas.reduce(image) do |memo, region|
66
- driver.draw_rectangles([memo], region, CapybaraScreenshotDiff::ORANGE_RGBA).first
67
- end
59
+ annotation_service.annotate_skip_areas(image, skip_areas)
68
60
  end
69
61
 
70
62
  def save(image, image_path)
71
- driver.save_image_to(image, image_path.to_s)
63
+ annotation_service.save(image, image_path)
64
+ end
65
+
66
+ def build_error_for_different_dimensions
67
+ change_msg = [comparison.base_image, comparison.new_image]
68
+ .map { |image| driver.dimension(image).join("x") }
69
+ .join(" => ")
70
+
71
+ "Dimensions have changed: #{change_msg}\n#{base_image_path.to_path}\n#{image_path.to_path}"
72
72
  end
73
73
 
74
74
  NEW_LINE = "\n"
@@ -85,20 +85,7 @@ module Capybara::Screenshot::Diff
85
85
 
86
86
  private
87
87
 
88
- def save_heatmap_diff
89
- merged_image = driver.merge(new_image, base_image)
90
- highlighted_mask = driver.highlight_mask(difference.diff_mask, merged_image, color: CapybaraScreenshotDiff::RED_RGBA)
91
-
92
- save(highlighted_mask, heatmap_diff_path.to_path)
93
- end
94
-
95
- def base_image
96
- difference.comparison.base_image
97
- end
98
-
99
- def new_image
100
- difference.comparison.new_image
101
- end
88
+ attr_reader :annotation_service
102
89
 
103
90
  def base_image_path
104
91
  comparison.base_image_path
@@ -14,7 +14,7 @@ module Capybara
14
14
  # `:stability_time_limit` and `:wait` in capture options and ensures that `:stability_time_limit` is less than or equal to `:wait`.
15
15
  #
16
16
  # @param capture_options [Hash] The options for capturing screenshots, must include `:stability_time_limit` and `:wait`.
17
- # @param comparison_options [Hash, nil] The options for comparing screenshots, defaults to `nil` which uses `Diff.default_options`.
17
+ # @param comparison_options [Hash] The options for comparing screenshots, defaults to `{}`. Same signature as {Capybara::Screenshot::Screenshoter#initialize}.
18
18
  # @raise [ArgumentError] If `:wait` or `:stability_time_limit` are not provided, or if `:stability_time_limit` is greater than `:wait`.
19
19
  def initialize(capture_options, comparison_options = {})
20
20
  @stability_time_limit, @wait = capture_options.fetch_values(*STABILITY_OPTIONS)
@@ -3,7 +3,7 @@
3
3
  module Capybara
4
4
  module Screenshot
5
5
  module Diff
6
- VERSION = "1.13.0"
6
+ VERSION = "1.14.0"
7
7
  end
8
8
  end
9
9
  end
@@ -10,9 +10,8 @@ Before do
10
10
  end
11
11
 
12
12
  After do |scenario|
13
- if !scenario.failed? && Capybara::Screenshot::Diff.pending_if_new && CapybaraScreenshotDiff.new_screenshots_present?
14
- names = CapybaraScreenshotDiff.new_screenshots
15
- skip_this_scenario("No baseline for: #{names.join(", ")}. Commit the captured screenshots to record them.")
13
+ if !scenario.failed? && (msg = CapybaraScreenshotDiff.pending_screenshots_message)
14
+ skip_this_scenario(msg)
16
15
  end
17
16
  ensure
18
17
  CapybaraScreenshotDiff.reset
@@ -36,9 +36,8 @@ module CapybaraScreenshotDiff
36
36
  super
37
37
  CapybaraScreenshotDiff.verify
38
38
 
39
- if ::Capybara::Screenshot::Diff.pending_if_new && CapybaraScreenshotDiff.new_screenshots_present?
40
- names = CapybaraScreenshotDiff.new_screenshots
41
- skip "No baseline for: #{names.join(", ")}. Commit the captured screenshots to record them."
39
+ if (msg = CapybaraScreenshotDiff.pending_screenshots_message)
40
+ skip(msg)
42
41
  end
43
42
  rescue CapybaraScreenshotDiff::ExpectationNotMet => e
44
43
  assertion = ::Minitest::Assertion.new(e)
@@ -36,9 +36,8 @@ RSpec.configure do |config|
36
36
  CapybaraScreenshotDiff.verify
37
37
 
38
38
  # Never mask a real failure with a pending marker.
39
- if example.exception.nil? && Capybara::Screenshot::Diff.pending_if_new && CapybaraScreenshotDiff.new_screenshots_present?
40
- names = CapybaraScreenshotDiff.new_screenshots
41
- skip "No baseline for: #{names.join(", ")}. Commit the captured screenshots to record them."
39
+ if example.exception.nil? && (msg = CapybaraScreenshotDiff.pending_screenshots_message)
40
+ skip(msg)
42
41
  end
43
42
  rescue CapybaraScreenshotDiff::ExpectationNotMet => e
44
43
  raise RSpec::Expectations::ExpectationNotMetError.new(e.message).tap { |ex| ex.set_backtrace(e.backtrace) }
@@ -158,6 +158,17 @@ module CapybaraScreenshotDiff
158
158
  def_delegator :registry, :screenshot_namer
159
159
  def_delegator :registry, :verify
160
160
 
161
+ # Message to skip the test with when a new screenshot has no baseline yet
162
+ # and `pending_if_new` is enabled. Adapters call this after verifying
163
+ # screenshots, and skip the test with the returned message when present.
164
+ #
165
+ # @return [String, nil] the pending message, or nil when there is nothing to report
166
+ def pending_screenshots_message
167
+ return unless ::Capybara::Screenshot::Diff.pending_if_new && new_screenshots_present?
168
+
169
+ "No baseline for: #{new_screenshots.join(", ")}. Commit the captured screenshots to record them."
170
+ end
171
+
161
172
  private
162
173
 
163
174
  def notify_reporters(assertions)
@@ -1,5 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Lazy load: snap_diff.rb requires this file, so eagerly `require`-ing it
4
+ # here would be circular. `autoload` defers the load until SnapDiff is
5
+ # first referenced, by which point this file has already finished loading.
6
+ autoload :SnapDiff, "snap_diff"
7
+
3
8
  require "capybara/dsl"
4
9
  require "capybara/screenshot/diff/version"
5
10
  require "capybara/screenshot/diff/utils"
data/lib/snap_diff.rb ADDED
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "capybara_screenshot_diff"
4
+
5
+ # Forward-looking namespace for the gem, per ADR-004.
6
+ #
7
+ # These are pure additive aliases onto the existing
8
+ # +Capybara::Screenshot::Diff+ API — no behavior changes, no deprecation
9
+ # warnings. See ADR-004 for the full migration plan.
10
+ module SnapDiff
11
+ Comparison = Capybara::Screenshot::Diff::ImageCompare
12
+
13
+ def self.compare(...)
14
+ Capybara::Screenshot::Diff.compare(...)
15
+ end
16
+
17
+ def self.start(&block)
18
+ Capybara::Screenshot::Diff.configure(&block)
19
+ end
20
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capybara-screenshot-diff
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.13.0
4
+ version: 1.14.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uwe Kubosch
@@ -98,12 +98,12 @@ files:
98
98
  - gems.rb
99
99
  - lib/capybara-screenshot-diff.rb
100
100
  - lib/capybara/screenshot/diff.rb
101
+ - lib/capybara/screenshot/diff/annotation_service.rb
101
102
  - lib/capybara/screenshot/diff/area_calculator.rb
102
103
  - lib/capybara/screenshot/diff/browser_helpers.rb
103
104
  - lib/capybara/screenshot/diff/comparison.rb
104
105
  - lib/capybara/screenshot/diff/cucumber.rb
105
106
  - lib/capybara/screenshot/diff/difference.rb
106
- - lib/capybara/screenshot/diff/difference_finder.rb
107
107
  - lib/capybara/screenshot/diff/drivers.rb
108
108
  - lib/capybara/screenshot/diff/drivers/base_driver.rb
109
109
  - lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb
@@ -134,6 +134,7 @@ files:
134
134
  - lib/capybara_screenshot_diff/snap.rb
135
135
  - lib/capybara_screenshot_diff/snap_manager.rb
136
136
  - lib/capybara_screenshot_diff/static.rb
137
+ - lib/snap_diff.rb
137
138
  homepage: https://github.com/snap-diff/snap_diff-capybara
138
139
  licenses:
139
140
  - MIT
@@ -1,97 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "capybara/screenshot/diff/comparison"
4
- require "capybara/screenshot/diff/difference"
5
-
6
- module Capybara
7
- module Screenshot
8
- module Diff
9
- # Analyzes image differences with configurable tolerance levels.
10
- #
11
- # This class implements the core comparison logic for detecting visual differences
12
- # between images while accounting for various tolerances and optimizations.
13
- #
14
- # The comparison process follows these steps:
15
- # 1. Dimension Check (Fastest)
16
- # - Compares image dimensions first for quick rejection
17
- # - Different dimensions always indicate a difference
18
- #
19
- # 2. Pixel Equality Check (Fast)
20
- # - Performs bitwise comparison if dimensions match
21
- # - Returns immediately if images are exactly identical
22
- #
23
- # 3. Tolerant Comparison (Slower)
24
- # - Only runs if quick checks don't determine equality
25
- # - Respects configured tolerances for color and shift differences
26
- # - Can ignore specific regions (skip_area)
27
- # - Considers anti-aliasing and sub-pixel rendering differences
28
- #
29
- # The class is designed to be stateless and thread-safe, with all configuration
30
- # passed in through the constructor.
31
- class DifferenceFinder
32
- TOLERABLE_OPTIONS = [:tolerance, :color_distance_limit, :shift_distance_limit, :area_size_limit].freeze
33
-
34
- attr_reader :driver, :options
35
-
36
- # Creates a new DifferenceFinder instance.
37
- #
38
- # @param driver [Drivers::Base] The image processing driver to use.
39
- # Must implement the driver interface expected by DifferenceFinder.
40
- # @param options [Hash] Configuration options for the comparison:
41
- # @option options [Numeric] :tolerance (0.001) Color tolerance threshold (0.0-1.0).
42
- # @option options [Numeric] :color_distance_limit Maximum allowed color distance.
43
- # @option options [Numeric] :shift_distance_limit Maximum allowed shift distance.
44
- # @option options [Numeric] :area_size_limit Maximum allowed difference area size.
45
- # @option options [Array<Array>] :skip_area Regions to exclude from comparison.
46
- def initialize(driver, options)
47
- @driver = driver
48
- @options = options
49
- @without_tolerable_options = (options.keys & TOLERABLE_OPTIONS).empty?
50
- end
51
-
52
- # Analyzes the comparison and determines if images are different.
53
- #
54
- # @param comparison [Comparison] The comparison object containing images to analyze.
55
- # @param quick_mode [Boolean] When true, performs minimal checks and returns early.
56
- # In quick mode, returns [is_equal, difference] where:
57
- # - is_equal is true if images are considered equal
58
- # - difference is a Difference object or nil
59
- # When false, returns a Difference object directly.
60
- # @return [Array, Difference] Result format depends on quick_mode parameter.
61
- # @raise [ArgumentError] If the comparison object is invalid.
62
- def call(comparison, quick_mode: true)
63
- # Process the comparison and return result
64
-
65
- # Handle dimension differences
66
- unless driver.same_dimension?(comparison)
67
- result = Difference.build_null(comparison, comparison.base_image_path, comparison.new_image_path, {different_dimensions: true})
68
- return quick_mode ? [false, result] : result
69
- end
70
-
71
- # Handle identical pixels
72
- if driver.same_pixels?(comparison)
73
- result = Difference.build_null(comparison, comparison.base_image_path, comparison.new_image_path)
74
- return quick_mode ? [true, result] : result
75
- end
76
-
77
- # Handle early return for non-tolerable options
78
- if quick_mode && without_tolerable_options?
79
- return [false, nil]
80
- end
81
-
82
- # Process difference region
83
- region = driver.find_difference_region(comparison)
84
-
85
- # Only create a proper difference object if we've completed the comparison
86
- quick_mode ? [!region.different?, region] : region
87
- end
88
-
89
- private
90
-
91
- def without_tolerable_options?
92
- @without_tolerable_options
93
- end
94
- end
95
- end
96
- end
97
- end