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 +4 -4
- data/CHANGELOG.md +16 -0
- data/docs/RELEASE_PREP.md +18 -16
- data/docs/UPGRADING.md +87 -2
- data/docs/architecture.md +1 -2
- data/lib/capybara/screenshot/diff/annotation_service.rb +79 -0
- data/lib/capybara/screenshot/diff/image_compare.rb +43 -6
- data/lib/capybara/screenshot/diff/reporters/default.rb +30 -43
- data/lib/capybara/screenshot/diff/stable_screenshoter.rb +1 -1
- data/lib/capybara/screenshot/diff/version.rb +1 -1
- data/lib/capybara_screenshot_diff/cucumber.rb +2 -3
- data/lib/capybara_screenshot_diff/minitest.rb +2 -3
- data/lib/capybara_screenshot_diff/rspec.rb +2 -3
- data/lib/capybara_screenshot_diff/screenshot_assertion.rb +11 -0
- data/lib/capybara_screenshot_diff.rb +5 -0
- data/lib/snap_diff.rb +20 -0
- metadata +3 -2
- data/lib/capybara/screenshot/diff/difference_finder.rb +0 -97
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dba172cde687794eeedbffe4c4112d58ed5c4e2fcac5cdaf93b84827396e832d
|
|
4
|
+
data.tar.gz: a6b45b9bfc4ff349b506c7045f54392101c1bbfecda7b51475758fd2ee1536c0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
1
|
+
# Release Preparation — v1.14.0
|
|
2
2
|
|
|
3
3
|
## Summary
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
14
|
-
- [x] Run tests: `bundle exec rake test:unit` (
|
|
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.
|
|
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
|
-
###
|
|
32
|
-
- `
|
|
33
|
-
|
|
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
|
-
###
|
|
38
|
-
- `
|
|
39
|
-
|
|
40
|
-
|
|
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
|
|
1
|
+
# Upgrading
|
|
2
2
|
|
|
3
|
-
##
|
|
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
|
-
-
|
|
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
|
|
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
|
|
114
|
-
@
|
|
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
|
-
|
|
134
|
-
|
|
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 :
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
55
|
+
annotation_service.annotate_difference(image, region)
|
|
62
56
|
end
|
|
63
57
|
|
|
64
58
|
def annotate_skip_areas(image, skip_areas)
|
|
65
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
@@ -10,9 +10,8 @@ Before do
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
After do |scenario|
|
|
13
|
-
if !scenario.failed? &&
|
|
14
|
-
|
|
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
|
|
40
|
-
|
|
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? &&
|
|
40
|
-
|
|
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.
|
|
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
|