capybara-screenshot-diff 1.13.0 → 1.15.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 +31 -0
- data/docs/RELEASE_PREP.md +18 -16
- data/docs/UPGRADING.md +87 -2
- data/docs/architecture.md +1 -4
- data/lib/capybara/screenshot/diff/annotation_service.rb +79 -0
- data/lib/capybara/screenshot/diff/image_compare.rb +50 -7
- 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.rb +1 -1
- data/lib/capybara_screenshot_diff/cucumber.rb +2 -3
- data/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb +19 -2
- data/lib/capybara_screenshot_diff/minitest.rb +14 -4
- data/lib/capybara_screenshot_diff/rspec.rb +19 -5
- data/lib/capybara_screenshot_diff/screenshot_assertion.rb +11 -0
- data/lib/capybara_screenshot_diff.rb +5 -0
- data/lib/snap_diff/config.rb +74 -0
- data/lib/snap_diff.rb +49 -0
- metadata +4 -4
- data/lib/capybara/screenshot/diff/comparison.rb +0 -9
- data/lib/capybara/screenshot/diff/difference_finder.rb +0 -97
- data/lib/capybara_screenshot_diff/backtrace_filter.rb +0 -20
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e22999718673e3e7cec926c524deedb3e3d0ead26e7c8350b777f5a453f02ad6
|
|
4
|
+
data.tar.gz: 1c10e980fb7787a493c763ddc3ee8f1a7ada5180af7277e389b24761a359f68d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a3328839042af4d915150aba0c135d52f99074b9c31a69bdf9f6ff7e2b5a4c2c352b94f767ed9ec24f517c232257d1111bce1db3f019a668b8b4ba5314b09b1
|
|
7
|
+
data.tar.gz: 30c9cabdbabf2823c7fe77b4b825f7d59d0e3a8b49d373544eabb3ad171bee58d9ca8e57c6555b93254e7f04d0aa04075dd84839f4623151ae818efdd020845d
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,37 @@ 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.15.0] - 2026-08-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **`SnapDiff::Config`** — one flat object consolidating all 27 settings behind `SnapDiff.config` / `SnapDiff.configure { |config| ... }`; purely additive, the existing accessors stay canonical and both views share one source of truth ([#200](https://github.com/snap-diff/snap_diff-capybara/pull/200))
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
- **`pending_if_new` no longer masks real failures** — a raising Minitest `teardown` or a raising RSpec `after` hook registered before the gem could previously be reported as a skipped/pending test with exit 0; the pending marker now defers until the framework's teardown chain has run and yields to any failure. Known residual: an RSpec `config.append_after` registered after this gem still runs later than the gem's hook — require the gem last if you use appended after-hooks ([#203](https://github.com/snap-diff/snap_diff-capybara/pull/203))
|
|
15
|
+
- **`BacktraceFilter` path-boundary matching** — a custom `lib_directory` of `/app/lib` no longer also filters locations under `/app/library` ([#202](https://github.com/snap-diff/snap_diff-capybara/pull/202))
|
|
16
|
+
|
|
17
|
+
### Internal
|
|
18
|
+
- Failure-masking guard tests for the `pending_if_new` teardown paths via subprocess RSpec runs ([#199](https://github.com/snap-diff/snap_diff-capybara/pull/199))
|
|
19
|
+
- Hygiene pass — skip-area/VCS-baseline and `BacktraceFilter` guard tests, entry-point forwarder dedupe, `Comparison` and `BacktraceFilter` merged into their sole consumers (−2 files, constant paths preserved) ([#201](https://github.com/snap-diff/snap_diff-capybara/pull/201))
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [v1.14.0] - 2026-08-22
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
- **`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))
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- **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))
|
|
30
|
+
|
|
31
|
+
### Internal
|
|
32
|
+
- Merged `DifferenceFinder` into `ImageCompare` (ADR-004 step) ([#192](https://github.com/snap-diff/snap_diff-capybara/pull/192))
|
|
33
|
+
- Extracted `AnnotationService` from `Reporters::Default`; full public surface preserved via delegation ([#193](https://github.com/snap-diff/snap_diff-capybara/pull/193))
|
|
34
|
+
- 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))
|
|
35
|
+
- Test-suite driver-coverage banner and CI guard against silently missing drivers ([#198](https://github.com/snap-diff/snap_diff-capybara/pull/198))
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
8
39
|
## [v1.13.0] - 2026-08-22
|
|
9
40
|
|
|
10
41
|
### Added
|
data/docs/RELEASE_PREP.md
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
|
-
# Release Preparation — v1.
|
|
1
|
+
# Release Preparation — v1.15.0
|
|
2
2
|
|
|
3
3
|
## Summary
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
for missing baselines.
|
|
5
|
+
Additive `SnapDiff::Config`, two failure-masking fixes in the `pending_if_new`
|
|
6
|
+
teardown paths, a `BacktraceFilter` boundary fix, and an internal hygiene pass.
|
|
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.15.0`
|
|
13
|
+
- [x] Run tests: `bundle exec rake test:unit` (267 runs, 0 failures)
|
|
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.15.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::Config` — flat, additive consolidation of all 27 settings
|
|
32
|
+
(`SnapDiff.config` / `SnapDiff.configure`); old accessors stay canonical
|
|
36
33
|
|
|
37
|
-
###
|
|
38
|
-
- `
|
|
39
|
-
|
|
40
|
-
|
|
34
|
+
### Fixed
|
|
35
|
+
- `pending_if_new` no longer converts real teardown/after-hook failures into
|
|
36
|
+
pending tests (Minitest defers to `after_teardown`; RSpec uses `append_after`;
|
|
37
|
+
known residual for consumer `append_after` hooks documented)
|
|
38
|
+
- `BacktraceFilter` custom `lib_directory` matches on a path boundary
|
|
39
|
+
|
|
40
|
+
### Internal
|
|
41
|
+
- Guard tests for failure masking and skip-area/VCS-baseline regressions;
|
|
42
|
+
two files merged into sole consumers (constant paths preserved)
|
|
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
|
|
|
@@ -239,7 +239,6 @@ lib/
|
|
|
239
239
|
screenshot_assertion.rb # Assertion + registry objects
|
|
240
240
|
attempts_reporter.rb # Debug reporting for unstable captures
|
|
241
241
|
error_with_filtered_backtrace.rb # Error with filtered stack
|
|
242
|
-
backtrace_filter.rb # Library/gem frame filtering
|
|
243
242
|
reporters/
|
|
244
243
|
html.rb # Interactive HTML report reporter
|
|
245
244
|
templates/report.html.erb # HTML report template
|
|
@@ -255,9 +254,7 @@ lib/
|
|
|
255
254
|
stable_screenshoter.rb # Stability detection wrapper
|
|
256
255
|
screenshot_matcher.rb # Orchestrator for capture + compare
|
|
257
256
|
image_compare.rb # Layered comparison engine
|
|
258
|
-
comparison.rb # Comparison value object
|
|
259
257
|
difference.rb # Difference result value object
|
|
260
|
-
difference_finder.rb # Core diff analysis logic
|
|
261
258
|
image_preprocessor.rb # Pre-processing (skip areas, median filter)
|
|
262
259
|
area_calculator.rb # Crop/skip area coordinate resolution
|
|
263
260
|
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
|
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
require "pathname"
|
|
4
4
|
require "fileutils"
|
|
5
5
|
|
|
6
|
-
require "capybara/screenshot/diff/
|
|
6
|
+
require "capybara/screenshot/diff/difference"
|
|
7
|
+
require "capybara/screenshot/diff/drivers"
|
|
7
8
|
require "capybara/screenshot/diff/image_preprocessor"
|
|
8
|
-
require "capybara/screenshot/diff/difference_finder"
|
|
9
9
|
require "capybara/screenshot/diff/reporters/default"
|
|
10
10
|
|
|
11
11
|
module Capybara
|
|
@@ -13,6 +13,13 @@ module Capybara
|
|
|
13
13
|
module Diff
|
|
14
14
|
LOADED_DRIVERS = {}
|
|
15
15
|
|
|
16
|
+
# Holds the two images (and their paths/options/driver) being compared.
|
|
17
|
+
class Comparison < Struct.new(:new_image, :base_image, :options, :driver, :new_image_path, :base_image_path)
|
|
18
|
+
def skip_area
|
|
19
|
+
options[:skip_area]
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
16
23
|
# Handles comparison of two images with a focus on performance and accuracy.
|
|
17
24
|
#
|
|
18
25
|
# This class implements a multi-layered optimization strategy for image comparison:
|
|
@@ -36,6 +43,8 @@ module Capybara
|
|
|
36
43
|
# - Only performing expensive operations when absolutely necessary
|
|
37
44
|
# - Maintaining high accuracy for complex comparisons
|
|
38
45
|
class ImageCompare
|
|
46
|
+
TOLERABLE_OPTIONS = [:tolerance, :color_distance_limit, :shift_distance_limit, :area_size_limit].freeze
|
|
47
|
+
|
|
39
48
|
attr_reader :driver, :driver_options
|
|
40
49
|
attr_reader :image_path, :base_image_path
|
|
41
50
|
attr_reader :difference, :error_message
|
|
@@ -48,6 +57,7 @@ module Capybara
|
|
|
48
57
|
|
|
49
58
|
@driver_options = options.freeze
|
|
50
59
|
@driver = Drivers.for(@driver_options)
|
|
60
|
+
@without_tolerable_options = (driver_options.keys & TOLERABLE_OPTIONS).empty?
|
|
51
61
|
end
|
|
52
62
|
|
|
53
63
|
# Performs a quick comparison of two image files.
|
|
@@ -85,7 +95,7 @@ module Capybara
|
|
|
85
95
|
# - `false` if the images are considered identical
|
|
86
96
|
#
|
|
87
97
|
# @see #processed
|
|
88
|
-
# @see
|
|
98
|
+
# @see #analyze_difference
|
|
89
99
|
def different?
|
|
90
100
|
processed.difference.different?
|
|
91
101
|
end
|
|
@@ -110,8 +120,8 @@ module Capybara
|
|
|
110
120
|
|
|
111
121
|
private
|
|
112
122
|
|
|
113
|
-
def
|
|
114
|
-
@
|
|
123
|
+
def without_tolerable_options?
|
|
124
|
+
@without_tolerable_options
|
|
115
125
|
end
|
|
116
126
|
|
|
117
127
|
def load_images_and_build_comparison(base_path, new_path, options)
|
|
@@ -130,8 +140,41 @@ module Capybara
|
|
|
130
140
|
# Create comparison with preprocessed images
|
|
131
141
|
comparison = load_comparison(base_image_path, image_path, driver_options)
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
|
|
143
|
+
analyze_difference(comparison, quick_mode: quick_mode)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Analyzes the comparison and determines if images are different.
|
|
147
|
+
#
|
|
148
|
+
# @param comparison [Comparison] The comparison object containing images to analyze.
|
|
149
|
+
# @param quick_mode [Boolean] When true, performs minimal checks and returns early.
|
|
150
|
+
# In quick mode, returns [is_equal, difference] where:
|
|
151
|
+
# - is_equal is true if images are considered equal
|
|
152
|
+
# - difference is a Difference object or nil
|
|
153
|
+
# When false, returns a Difference object directly.
|
|
154
|
+
# @return [Array, Difference] Result format depends on quick_mode parameter.
|
|
155
|
+
def analyze_difference(comparison, quick_mode: true)
|
|
156
|
+
# Handle dimension differences
|
|
157
|
+
unless driver.same_dimension?(comparison)
|
|
158
|
+
result = Difference.build_null(comparison, comparison.base_image_path, comparison.new_image_path, {different_dimensions: true})
|
|
159
|
+
return quick_mode ? [false, result] : result
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
# Handle identical pixels
|
|
163
|
+
if driver.same_pixels?(comparison)
|
|
164
|
+
result = Difference.build_null(comparison, comparison.base_image_path, comparison.new_image_path)
|
|
165
|
+
return quick_mode ? [true, result] : result
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Handle early return for non-tolerable options
|
|
169
|
+
if quick_mode && without_tolerable_options?
|
|
170
|
+
return [false, nil]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Process difference region
|
|
174
|
+
region = driver.find_difference_region(comparison)
|
|
175
|
+
|
|
176
|
+
# Only create a proper difference object if we've completed the comparison
|
|
177
|
+
quick_mode ? [!region.different?, region] : region
|
|
135
178
|
end
|
|
136
179
|
|
|
137
180
|
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
|
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "capybara_screenshot_diff/backtrace_filter"
|
|
4
|
-
|
|
5
3
|
module CapybaraScreenshotDiff
|
|
4
|
+
class BacktraceFilter
|
|
5
|
+
LIB_DIRECTORY = File.expand_path(File.join(File.dirname(__FILE__), "..")) + File::SEPARATOR
|
|
6
|
+
|
|
7
|
+
def initialize(lib_directory = LIB_DIRECTORY)
|
|
8
|
+
# Trailing separator keeps the prefix match on a directory boundary,
|
|
9
|
+
# so "/app/lib" does not also reject "/app/library".
|
|
10
|
+
@lib_directory = lib_directory.end_with?(File::SEPARATOR) ? lib_directory : lib_directory + File::SEPARATOR
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Filters out any backtrace lines originating from the library directory or from gems such as ActiveSupport, Minitest, and Railties
|
|
14
|
+
# @param backtrace [Array<String>]
|
|
15
|
+
# @return [Array<String>]
|
|
16
|
+
def filtered(backtrace)
|
|
17
|
+
backtrace
|
|
18
|
+
.reject { |location| File.expand_path(location).start_with?(@lib_directory) }
|
|
19
|
+
.reject { |l| l =~ /gems\/(activesupport|minitest|railties)/ }
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
6
23
|
# @private
|
|
7
24
|
class ErrorWithFilteredBacktrace < StandardError
|
|
8
25
|
# @private
|
|
@@ -36,10 +36,10 @@ module CapybaraScreenshotDiff
|
|
|
36
36
|
super
|
|
37
37
|
CapybaraScreenshotDiff.verify
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
# Computed here (before teardown/reset), but the actual `skip` is
|
|
40
|
+
# deferred to `after_teardown` so a real error raised by the user's
|
|
41
|
+
# `teardown` isn't masked by a pending skip recorded before it ran.
|
|
42
|
+
@capybara_screenshot_diff_pending_message = CapybaraScreenshotDiff.pending_screenshots_message
|
|
43
43
|
rescue CapybaraScreenshotDiff::ExpectationNotMet => e
|
|
44
44
|
assertion = ::Minitest::Assertion.new(e)
|
|
45
45
|
assertion.set_backtrace(e.backtrace)
|
|
@@ -47,6 +47,16 @@ module CapybaraScreenshotDiff
|
|
|
47
47
|
ensure
|
|
48
48
|
CapybaraScreenshotDiff.reset
|
|
49
49
|
end
|
|
50
|
+
|
|
51
|
+
def after_teardown
|
|
52
|
+
super
|
|
53
|
+
|
|
54
|
+
# Never mask a real failure (from `verify` above or from the user's
|
|
55
|
+
# own `teardown`) with a pending marker.
|
|
56
|
+
if failures.empty? && (msg = @capybara_screenshot_diff_pending_message)
|
|
57
|
+
skip(msg)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
50
60
|
end
|
|
51
61
|
end
|
|
52
62
|
end
|
|
@@ -30,15 +30,29 @@ RSpec.configure do |config|
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
# `append_after` (as opposed to the default `after`, which prepends) adds
|
|
34
|
+
# this hook to the *end* of the after-hook chain regardless of when it's
|
|
35
|
+
# registered relative to the user's own `after`/`config.after` hooks. RSpec
|
|
36
|
+
# runs `after(:each)` hooks in reverse registration order, so a plain
|
|
37
|
+
# `config.after` here would run BEFORE a user hook registered earlier in
|
|
38
|
+
# their own spec_helper (before this file was required) -- and if that
|
|
39
|
+
# later-running user hook raises, its exception gets folded into
|
|
40
|
+
# `pending_exception` instead of `example.exception`, silently masking the
|
|
41
|
+
# failure behind our pending skip. `append_after` runs after the full user
|
|
42
|
+
# after-chain no matter the registration order, closing that gap.
|
|
43
|
+
config.append_after do |example|
|
|
34
44
|
if self.class.include?(CapybaraScreenshotDiff::DSL)
|
|
35
45
|
begin
|
|
36
46
|
CapybaraScreenshotDiff.verify
|
|
37
47
|
|
|
38
|
-
# Never mask a real failure with a pending marker.
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
# Never mask a real failure with a pending marker. Kept as
|
|
49
|
+
# defense-in-depth: `append_after` observes failures from plain
|
|
50
|
+
# `after`/`prepend_after` user hooks, but appended hooks run FIFO,
|
|
51
|
+
# so a user `append_after` registered after this gem still runs
|
|
52
|
+
# later than us — RSpec has no "run absolutely last" construct.
|
|
53
|
+
# Mitigation for such consumers: require this gem last.
|
|
54
|
+
if example.exception.nil? && (msg = CapybaraScreenshotDiff.pending_screenshots_message)
|
|
55
|
+
skip(msg)
|
|
42
56
|
end
|
|
43
57
|
rescue CapybaraScreenshotDiff::ExpectationNotMet => e
|
|
44
58
|
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"
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# The MAPPING below references the legacy modules, so they must be loaded first.
|
|
4
|
+
require "capybara_screenshot_diff"
|
|
5
|
+
|
|
6
|
+
module SnapDiff
|
|
7
|
+
# Flat, additive consolidation of every existing
|
|
8
|
+
# +Capybara::Screenshot+ / +Capybara::Screenshot::Diff+ +mattr_accessor+
|
|
9
|
+
# setting behind one object: <tt>SnapDiff.config.<em>attr</em></tt>.
|
|
10
|
+
#
|
|
11
|
+
# Storage ownership: the OLD mattr_accessors remain the single source of
|
|
12
|
+
# truth. Config holds no state of its own -- every reader/writer defined
|
|
13
|
+
# from {MAPPING} simply forwards to the existing accessor. This is
|
|
14
|
+
# deliberate, not just simplest: several of those accessors carry default
|
|
15
|
+
# logic with observable timing (+fail_if_new+ derives its default from
|
|
16
|
+
# +ENV["CI"]+, +root+ falls back to +Rails.root+) that Rails' +mattr_accessor+
|
|
17
|
+
# evaluates once, at class-body-eval time, when +capybara_screenshot_diff.rb+
|
|
18
|
+
# first loads. Re-implementing that logic here -- even faithfully -- would
|
|
19
|
+
# mean evaluating it again, at a different moment (Config#new time), which
|
|
20
|
+
# is exactly the kind of "when defaults are evaluated" divergence the v2
|
|
21
|
+
# consolidation must not introduce. Delegating sidesteps the question
|
|
22
|
+
# entirely: Config never evaluates a default, it only ever forwards to
|
|
23
|
+
# whichever accessor already owns one. Bidirectional consistency (a write
|
|
24
|
+
# through either the old accessor or Config is visible through the other)
|
|
25
|
+
# falls out for free, because both paths read and write the exact same
|
|
26
|
+
# class-variable-backed storage.
|
|
27
|
+
class Config
|
|
28
|
+
# config attr name => [owning module, mattr_accessor name].
|
|
29
|
+
#
|
|
30
|
+
# The two names differ only for +screenshot_enabled+:
|
|
31
|
+
# +Capybara::Screenshot.enabled+ and +Capybara::Screenshot::Diff.enabled+
|
|
32
|
+
# are independent settings (see +Capybara::Screenshot.active?+, which
|
|
33
|
+
# reads both) that happen to share a bare name in their own modules. A
|
|
34
|
+
# flat Config can't expose two attributes both called +enabled+, so the
|
|
35
|
+
# Screenshot-side one is renamed here; Diff's keeps the bare +enabled+
|
|
36
|
+
# name since it's the one most existing configuration touches directly.
|
|
37
|
+
MAPPING = {
|
|
38
|
+
# Capybara::Screenshot
|
|
39
|
+
add_driver_path: [Capybara::Screenshot, :add_driver_path],
|
|
40
|
+
add_os_path: [Capybara::Screenshot, :add_os_path],
|
|
41
|
+
blur_active_element: [Capybara::Screenshot, :blur_active_element],
|
|
42
|
+
screenshot_enabled: [Capybara::Screenshot, :enabled],
|
|
43
|
+
hide_caret: [Capybara::Screenshot, :hide_caret],
|
|
44
|
+
disable_animations: [Capybara::Screenshot, :disable_animations],
|
|
45
|
+
root: [Capybara::Screenshot, :root],
|
|
46
|
+
stability_time_limit: [Capybara::Screenshot, :stability_time_limit],
|
|
47
|
+
window_size: [Capybara::Screenshot, :window_size],
|
|
48
|
+
save_path: [Capybara::Screenshot, :save_path],
|
|
49
|
+
use_lfs: [Capybara::Screenshot, :use_lfs],
|
|
50
|
+
screenshot_format: [Capybara::Screenshot, :screenshot_format],
|
|
51
|
+
capybara_screenshot_options: [Capybara::Screenshot, :capybara_screenshot_options],
|
|
52
|
+
# Capybara::Screenshot::Diff
|
|
53
|
+
delayed: [Capybara::Screenshot::Diff, :delayed],
|
|
54
|
+
area_size_limit: [Capybara::Screenshot::Diff, :area_size_limit],
|
|
55
|
+
fail_if_new: [Capybara::Screenshot::Diff, :fail_if_new],
|
|
56
|
+
pending_if_new: [Capybara::Screenshot::Diff, :pending_if_new],
|
|
57
|
+
fail_on_difference: [Capybara::Screenshot::Diff, :fail_on_difference],
|
|
58
|
+
color_distance_limit: [Capybara::Screenshot::Diff, :color_distance_limit],
|
|
59
|
+
enabled: [Capybara::Screenshot::Diff, :enabled],
|
|
60
|
+
shift_distance_limit: [Capybara::Screenshot::Diff, :shift_distance_limit],
|
|
61
|
+
skip_area: [Capybara::Screenshot::Diff, :skip_area],
|
|
62
|
+
driver: [Capybara::Screenshot::Diff, :driver],
|
|
63
|
+
tolerance: [Capybara::Screenshot::Diff, :tolerance],
|
|
64
|
+
perceptual_threshold: [Capybara::Screenshot::Diff, :perceptual_threshold],
|
|
65
|
+
screenshoter: [Capybara::Screenshot::Diff, :screenshoter],
|
|
66
|
+
manager: [Capybara::Screenshot::Diff, :manager]
|
|
67
|
+
}.freeze
|
|
68
|
+
|
|
69
|
+
MAPPING.each do |name, (mod, mattr)|
|
|
70
|
+
define_method(name) { mod.public_send(mattr) }
|
|
71
|
+
define_method(:"#{name}=") { |value| mod.public_send(:"#{mattr}=", value) }
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
data/lib/snap_diff.rb
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "capybara_screenshot_diff"
|
|
4
|
+
require "snap_diff/config"
|
|
5
|
+
|
|
6
|
+
# Forward-looking namespace for the gem, per ADR-004.
|
|
7
|
+
#
|
|
8
|
+
# These are pure additive aliases onto the existing
|
|
9
|
+
# +Capybara::Screenshot::Diff+ API — no behavior changes, no deprecation
|
|
10
|
+
# warnings. See ADR-004 for the full migration plan.
|
|
11
|
+
module SnapDiff
|
|
12
|
+
Comparison = Capybara::Screenshot::Diff::ImageCompare
|
|
13
|
+
|
|
14
|
+
def self.compare(...)
|
|
15
|
+
Capybara::Screenshot::Diff.compare(...)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# v1-style configuration: yields the two existing mattr_accessor holders
|
|
19
|
+
# (+Capybara::Screenshot+, +Capybara::Screenshot::Diff+) exactly as
|
|
20
|
+
# +Capybara::Screenshot::Diff.configure+ always has. Kept byte-for-byte
|
|
21
|
+
# identical to +Diff.configure+ for existing callers migrating namespaces
|
|
22
|
+
# without changing call shape.
|
|
23
|
+
#
|
|
24
|
+
# SnapDiff.start do |screenshot, diff|
|
|
25
|
+
# screenshot.window_size = [1280, 1024]
|
|
26
|
+
# diff.tolerance = 0.0005
|
|
27
|
+
# end
|
|
28
|
+
def self.start(&block)
|
|
29
|
+
Capybara::Screenshot::Diff.configure(&block)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Forward-looking configuration: yields the single consolidated
|
|
33
|
+
# {SnapDiff::Config} object instead of the two old holders. Same
|
|
34
|
+
# underlying storage as +start+ / the old mattr_accessors -- this is a
|
|
35
|
+
# different *shape* of the same settings, not a second source of truth.
|
|
36
|
+
#
|
|
37
|
+
# SnapDiff.configure do |config|
|
|
38
|
+
# config.window_size = [1280, 1024]
|
|
39
|
+
# config.tolerance = 0.0005
|
|
40
|
+
# end
|
|
41
|
+
def self.configure
|
|
42
|
+
yield config
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# The single consolidated settings object. See {SnapDiff::Config}.
|
|
46
|
+
def self.config
|
|
47
|
+
@config ||= Config.new
|
|
48
|
+
end
|
|
49
|
+
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.15.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Uwe Kubosch
|
|
@@ -98,12 +98,11 @@ 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
|
-
- lib/capybara/screenshot/diff/comparison.rb
|
|
104
104
|
- lib/capybara/screenshot/diff/cucumber.rb
|
|
105
105
|
- lib/capybara/screenshot/diff/difference.rb
|
|
106
|
-
- lib/capybara/screenshot/diff/difference_finder.rb
|
|
107
106
|
- lib/capybara/screenshot/diff/drivers.rb
|
|
108
107
|
- lib/capybara/screenshot/diff/drivers/base_driver.rb
|
|
109
108
|
- lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb
|
|
@@ -121,7 +120,6 @@ files:
|
|
|
121
120
|
- lib/capybara/screenshot/diff/version.rb
|
|
122
121
|
- lib/capybara_screenshot_diff.rb
|
|
123
122
|
- lib/capybara_screenshot_diff/attempts_reporter.rb
|
|
124
|
-
- lib/capybara_screenshot_diff/backtrace_filter.rb
|
|
125
123
|
- lib/capybara_screenshot_diff/cucumber.rb
|
|
126
124
|
- lib/capybara_screenshot_diff/dsl.rb
|
|
127
125
|
- lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb
|
|
@@ -134,6 +132,8 @@ files:
|
|
|
134
132
|
- lib/capybara_screenshot_diff/snap.rb
|
|
135
133
|
- lib/capybara_screenshot_diff/snap_manager.rb
|
|
136
134
|
- lib/capybara_screenshot_diff/static.rb
|
|
135
|
+
- lib/snap_diff.rb
|
|
136
|
+
- lib/snap_diff/config.rb
|
|
137
137
|
homepage: https://github.com/snap-diff/snap_diff-capybara
|
|
138
138
|
licenses:
|
|
139
139
|
- 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
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module CapybaraScreenshotDiff
|
|
4
|
-
class BacktraceFilter
|
|
5
|
-
LIB_DIRECTORY = File.expand_path(File.join(File.dirname(__FILE__), "..")) + File::SEPARATOR
|
|
6
|
-
|
|
7
|
-
def initialize(lib_directory = LIB_DIRECTORY)
|
|
8
|
-
@lib_directory = lib_directory
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
# Filters out any backtrace lines originating from the library directory or from gems such as ActiveSupport, Minitest, and Railties
|
|
12
|
-
# @param backtrace [Array<String>]
|
|
13
|
-
# @return [Array<String>]
|
|
14
|
-
def filtered(backtrace)
|
|
15
|
-
backtrace
|
|
16
|
-
.reject { |location| File.expand_path(location).start_with?(@lib_directory) }
|
|
17
|
-
.reject { |l| l =~ /gems\/(activesupport|minitest|railties)/ }
|
|
18
|
-
end
|
|
19
|
-
end
|
|
20
|
-
end
|