capybara-screenshot-diff 1.12.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 +29 -0
- data/CODE_OF_CONDUCT.md +129 -0
- data/docs/RELEASE_PREP.md +20 -34
- data/docs/UPGRADING.md +87 -2
- data/docs/architecture.md +269 -0
- data/docs/ci-integration.md +107 -77
- data/docs/configuration.md +16 -0
- data/docs/images/snap_diff_annotated.png +0 -0
- data/docs/migration-guide.md +286 -0
- data/docs/organization.md +1 -23
- 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/screenshot_matcher.rb +15 -1
- 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 +8 -0
- data/lib/capybara_screenshot_diff/dsl.rb +28 -7
- data/lib/capybara_screenshot_diff/minitest.rb +5 -3
- data/lib/capybara_screenshot_diff/rspec.rb +7 -2
- data/lib/capybara_screenshot_diff/screenshot_assertion.rb +25 -1
- data/lib/capybara_screenshot_diff.rb +6 -0
- data/lib/snap_diff.rb +20 -0
- metadata +8 -3
- data/lib/capybara/screenshot/diff/difference_finder.rb +0 -97
|
@@ -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
|
|
@@ -32,11 +32,25 @@ module Capybara
|
|
|
32
32
|
|
|
33
33
|
# Pre-computation: No need to compare without base screenshot
|
|
34
34
|
# NOTE: Consider to return PreValid Assertion Value Object with hard coded valid result
|
|
35
|
-
|
|
35
|
+
unless need_to_compare?
|
|
36
|
+
CapybaraScreenshotDiff.record_new_screenshot(screenshot_full_name)
|
|
37
|
+
return
|
|
38
|
+
end
|
|
36
39
|
|
|
37
40
|
create_screenshot_assertion(skip_stack_frames + 1, comparison_options)
|
|
38
41
|
end
|
|
39
42
|
|
|
43
|
+
# Captures a screenshot without comparing it to a baseline.
|
|
44
|
+
def capture
|
|
45
|
+
check_window_size!
|
|
46
|
+
prepare_screenshot_options
|
|
47
|
+
|
|
48
|
+
capture_options, comparison_options = extract_capture_and_comparison_options!(driver_options)
|
|
49
|
+
|
|
50
|
+
@snapshot.manager.create_output_directory_for(@snapshot.path)
|
|
51
|
+
capture_screenshot(capture_options, comparison_options)
|
|
52
|
+
end
|
|
53
|
+
|
|
40
54
|
private
|
|
41
55
|
|
|
42
56
|
def need_to_compare?
|
|
@@ -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)
|
|
@@ -9,4 +9,12 @@ Before do
|
|
|
9
9
|
Capybara::Screenshot::BrowserHelpers.resize_window_if_needed
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
After do |scenario|
|
|
13
|
+
if !scenario.failed? && (msg = CapybaraScreenshotDiff.pending_screenshots_message)
|
|
14
|
+
skip_this_scenario(msg)
|
|
15
|
+
end
|
|
16
|
+
ensure
|
|
17
|
+
CapybaraScreenshotDiff.reset
|
|
18
|
+
end
|
|
19
|
+
|
|
12
20
|
AfterAll { CapybaraScreenshotDiff.finalize_reporters! }
|
|
@@ -25,7 +25,7 @@ module CapybaraScreenshotDiff
|
|
|
25
25
|
screenshot_namer.group = name
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
-
# Takes a screenshot and
|
|
28
|
+
# Takes a screenshot and compares it against a baseline image.
|
|
29
29
|
#
|
|
30
30
|
# The method follows a layered optimization strategy for comparison:
|
|
31
31
|
# 1. First checks if screenshot functionality is active
|
|
@@ -53,7 +53,7 @@ module CapybaraScreenshotDiff
|
|
|
53
53
|
# @raise [CapybaraScreenshotDiff::ExpectationNotMet] If comparison fails and immediate validation is enabled.
|
|
54
54
|
# @raise [CapybaraScreenshotDiff::UnstableImage] If the image comparison is unstable.
|
|
55
55
|
# @raise [CapybaraScreenshotDiff::WindowSizeMismatchError] If the window size doesn't match expectations.
|
|
56
|
-
def
|
|
56
|
+
def assert_matches_screenshot(name, skip_stack_frames: 0, **options)
|
|
57
57
|
return false unless Capybara::Screenshot.active?
|
|
58
58
|
|
|
59
59
|
# Get the full name with section and group information
|
|
@@ -76,15 +76,36 @@ module CapybaraScreenshotDiff
|
|
|
76
76
|
true
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
#
|
|
80
|
-
# @
|
|
81
|
-
|
|
79
|
+
# Convenience wrapper around {#assert_matches_screenshot} and {#capture_screenshot}.
|
|
80
|
+
# @param compare [Boolean] When false, only captures the screenshot without comparing it to a baseline.
|
|
81
|
+
# @see #assert_matches_screenshot
|
|
82
|
+
# @see #capture_screenshot
|
|
83
|
+
def screenshot(name, skip_stack_frames: 0, compare: true, **options)
|
|
84
|
+
if compare
|
|
85
|
+
assert_matches_screenshot(name, skip_stack_frames: skip_stack_frames + 1, **options)
|
|
86
|
+
else
|
|
87
|
+
capture_screenshot(name, **options)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Captures a screenshot without comparing it to a baseline.
|
|
92
|
+
# @param name [String] The base name of the screenshot, used to generate the filename.
|
|
93
|
+
# @param options [Hash] Additional options for taking the screenshot. See {#assert_matches_screenshot}.
|
|
94
|
+
# @return [Boolean] True if the screenshot was successfully captured.
|
|
95
|
+
def capture_screenshot(name, **options)
|
|
96
|
+
return false unless Capybara::Screenshot.active?
|
|
97
|
+
|
|
98
|
+
full_name = CapybaraScreenshotDiff.screenshot_namer.full_name(name)
|
|
99
|
+
Capybara::Screenshot::Diff::ScreenshotMatcher.new(full_name, options).capture
|
|
100
|
+
|
|
101
|
+
true
|
|
102
|
+
end
|
|
82
103
|
|
|
83
104
|
# Asserts the current page has no visual changes from the baseline.
|
|
84
105
|
# Override in your base test class to add project-specific behavior
|
|
85
106
|
# (e.g., waiting for Turbo, default skip areas).
|
|
86
107
|
def assert_no_screenshot_changes(name, skip_stack_frames: 0, **opts)
|
|
87
|
-
|
|
108
|
+
assert_matches_screenshot(name, skip_stack_frames: skip_stack_frames + 1, **opts)
|
|
88
109
|
end
|
|
89
110
|
|
|
90
111
|
private
|
|
@@ -96,7 +117,7 @@ module CapybaraScreenshotDiff
|
|
|
96
117
|
#
|
|
97
118
|
# @param name [String] The full name of the screenshot, including any section/group context.
|
|
98
119
|
# @param options [Hash] Options for screenshot taking and comparison.
|
|
99
|
-
# See {#
|
|
120
|
+
# See {#assert_matches_screenshot} for available options.
|
|
100
121
|
# @param skip_stack_frames [Integer] Number of stack frames to skip for error reporting.
|
|
101
122
|
# @return [ScreenshotAssertion, nil] The assertion object or nil if no assertion is needed.
|
|
102
123
|
# @see ScreenshotAssertion
|
|
@@ -19,7 +19,7 @@ module CapybaraScreenshotDiff
|
|
|
19
19
|
module Assertions
|
|
20
20
|
include ::CapybaraScreenshotDiff::DSL
|
|
21
21
|
|
|
22
|
-
def
|
|
22
|
+
def assert_matches_screenshot(*args, skip_stack_frames: 0, **opts)
|
|
23
23
|
self.assertions += 1
|
|
24
24
|
|
|
25
25
|
super(*args, skip_stack_frames: skip_stack_frames + 1, **opts)
|
|
@@ -27,8 +27,6 @@ module CapybaraScreenshotDiff
|
|
|
27
27
|
raise ::Minitest::Assertion, e.message
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
alias_method :assert_matches_screenshot, :screenshot
|
|
31
|
-
|
|
32
30
|
def setup
|
|
33
31
|
super
|
|
34
32
|
::Capybara::Screenshot::BrowserHelpers.resize_window_if_needed
|
|
@@ -37,6 +35,10 @@ module CapybaraScreenshotDiff
|
|
|
37
35
|
def before_teardown
|
|
38
36
|
super
|
|
39
37
|
CapybaraScreenshotDiff.verify
|
|
38
|
+
|
|
39
|
+
if (msg = CapybaraScreenshotDiff.pending_screenshots_message)
|
|
40
|
+
skip(msg)
|
|
41
|
+
end
|
|
40
42
|
rescue CapybaraScreenshotDiff::ExpectationNotMet => e
|
|
41
43
|
assertion = ::Minitest::Assertion.new(e)
|
|
42
44
|
assertion.set_backtrace(e.backtrace)
|
|
@@ -7,7 +7,7 @@ RSpec::Matchers.define :match_screenshot do |name, **options|
|
|
|
7
7
|
description { "match screenshot '#{name}'" }
|
|
8
8
|
|
|
9
9
|
match do |_page|
|
|
10
|
-
|
|
10
|
+
assert_matches_screenshot(name, **options)
|
|
11
11
|
true
|
|
12
12
|
end
|
|
13
13
|
|
|
@@ -30,10 +30,15 @@ RSpec.configure do |config|
|
|
|
30
30
|
end
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
config.after do
|
|
33
|
+
config.after do |example|
|
|
34
34
|
if self.class.include?(CapybaraScreenshotDiff::DSL)
|
|
35
35
|
begin
|
|
36
36
|
CapybaraScreenshotDiff.verify
|
|
37
|
+
|
|
38
|
+
# Never mask a real failure with a pending marker.
|
|
39
|
+
if example.exception.nil? && (msg = CapybaraScreenshotDiff.pending_screenshots_message)
|
|
40
|
+
skip(msg)
|
|
41
|
+
end
|
|
37
42
|
rescue CapybaraScreenshotDiff::ExpectationNotMet => e
|
|
38
43
|
raise RSpec::Expectations::ExpectationNotMetError.new(e.message).tap { |ex| ex.set_backtrace(e.backtrace) }
|
|
39
44
|
ensure
|
|
@@ -65,10 +65,11 @@ module CapybaraScreenshotDiff
|
|
|
65
65
|
end
|
|
66
66
|
|
|
67
67
|
class AssertionRegistry
|
|
68
|
-
attr_reader :assertions, :screenshot_namer
|
|
68
|
+
attr_reader :assertions, :screenshot_namer, :new_screenshots
|
|
69
69
|
|
|
70
70
|
def initialize
|
|
71
71
|
@assertions = []
|
|
72
|
+
@new_screenshots = []
|
|
72
73
|
@screenshot_namer = CapybaraScreenshotDiff::ScreenshotNamer.new
|
|
73
74
|
end
|
|
74
75
|
|
|
@@ -84,6 +85,14 @@ module CapybaraScreenshotDiff
|
|
|
84
85
|
!@assertions.empty?
|
|
85
86
|
end
|
|
86
87
|
|
|
88
|
+
def record_new_screenshot(name)
|
|
89
|
+
@new_screenshots.push(name)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def new_screenshots_present?
|
|
93
|
+
!@new_screenshots.empty?
|
|
94
|
+
end
|
|
95
|
+
|
|
87
96
|
def verify(screenshots = CapybaraScreenshotDiff.assertions)
|
|
88
97
|
return unless ::Capybara::Screenshot.active? && ::Capybara::Screenshot::Diff.fail_on_difference
|
|
89
98
|
|
|
@@ -102,6 +111,7 @@ module CapybaraScreenshotDiff
|
|
|
102
111
|
|
|
103
112
|
def reset
|
|
104
113
|
@assertions.clear
|
|
114
|
+
@new_screenshots.clear
|
|
105
115
|
@screenshot_namer = CapybaraScreenshotDiff::ScreenshotNamer.new
|
|
106
116
|
end
|
|
107
117
|
end
|
|
@@ -120,6 +130,9 @@ module CapybaraScreenshotDiff
|
|
|
120
130
|
def_delegator :registry, :assertions
|
|
121
131
|
def_delegator :registry, :assertions_present?
|
|
122
132
|
def_delegator :registry, :failed_assertions
|
|
133
|
+
def_delegator :registry, :record_new_screenshot
|
|
134
|
+
def_delegator :registry, :new_screenshots
|
|
135
|
+
def_delegator :registry, :new_screenshots_present?
|
|
123
136
|
def reset
|
|
124
137
|
notify_reporters(registry.assertions)
|
|
125
138
|
registry.reset
|
|
@@ -145,6 +158,17 @@ module CapybaraScreenshotDiff
|
|
|
145
158
|
def_delegator :registry, :screenshot_namer
|
|
146
159
|
def_delegator :registry, :verify
|
|
147
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
|
+
|
|
148
172
|
private
|
|
149
173
|
|
|
150
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"
|
|
@@ -65,6 +70,7 @@ module Capybara
|
|
|
65
70
|
mattr_accessor(:delayed) { true }
|
|
66
71
|
mattr_accessor :area_size_limit
|
|
67
72
|
mattr_accessor(:fail_if_new) { !ENV["CI"].nil? && !ENV["CI"].empty? }
|
|
73
|
+
mattr_accessor(:pending_if_new) { false }
|
|
68
74
|
mattr_accessor(:fail_on_difference) { true }
|
|
69
75
|
mattr_accessor :color_distance_limit
|
|
70
76
|
mattr_accessor(:enabled) { true }
|
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
|
|
@@ -77,29 +77,33 @@ extensions: []
|
|
|
77
77
|
extra_rdoc_files: []
|
|
78
78
|
files:
|
|
79
79
|
- CHANGELOG.md
|
|
80
|
+
- CODE_OF_CONDUCT.md
|
|
80
81
|
- LICENSE.txt
|
|
81
82
|
- Rakefile
|
|
82
83
|
- capybara-screenshot-diff.gemspec
|
|
83
84
|
- docs/RELEASE_PREP.md
|
|
84
85
|
- docs/UPGRADING.md
|
|
86
|
+
- docs/architecture.md
|
|
85
87
|
- docs/ci-integration.md
|
|
86
88
|
- docs/configuration.md
|
|
87
89
|
- docs/docker-testing.md
|
|
88
90
|
- docs/drivers.md
|
|
89
91
|
- docs/framework-setup.md
|
|
92
|
+
- docs/images/snap_diff_annotated.png
|
|
90
93
|
- docs/images/snap_diff_web_ui.png
|
|
94
|
+
- docs/migration-guide.md
|
|
91
95
|
- docs/organization.md
|
|
92
96
|
- docs/reporters.md
|
|
93
97
|
- docs/thread_safety.md
|
|
94
98
|
- gems.rb
|
|
95
99
|
- lib/capybara-screenshot-diff.rb
|
|
96
100
|
- lib/capybara/screenshot/diff.rb
|
|
101
|
+
- lib/capybara/screenshot/diff/annotation_service.rb
|
|
97
102
|
- lib/capybara/screenshot/diff/area_calculator.rb
|
|
98
103
|
- lib/capybara/screenshot/diff/browser_helpers.rb
|
|
99
104
|
- lib/capybara/screenshot/diff/comparison.rb
|
|
100
105
|
- lib/capybara/screenshot/diff/cucumber.rb
|
|
101
106
|
- lib/capybara/screenshot/diff/difference.rb
|
|
102
|
-
- lib/capybara/screenshot/diff/difference_finder.rb
|
|
103
107
|
- lib/capybara/screenshot/diff/drivers.rb
|
|
104
108
|
- lib/capybara/screenshot/diff/drivers/base_driver.rb
|
|
105
109
|
- lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb
|
|
@@ -130,6 +134,7 @@ files:
|
|
|
130
134
|
- lib/capybara_screenshot_diff/snap.rb
|
|
131
135
|
- lib/capybara_screenshot_diff/snap_manager.rb
|
|
132
136
|
- lib/capybara_screenshot_diff/static.rb
|
|
137
|
+
- lib/snap_diff.rb
|
|
133
138
|
homepage: https://github.com/snap-diff/snap_diff-capybara
|
|
134
139
|
licenses:
|
|
135
140
|
- MIT
|
|
@@ -149,7 +154,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
149
154
|
- !ruby/object:Gem::Version
|
|
150
155
|
version: '0'
|
|
151
156
|
requirements: []
|
|
152
|
-
rubygems_version: 4.0.
|
|
157
|
+
rubygems_version: 4.0.16
|
|
153
158
|
specification_version: 4
|
|
154
159
|
summary: Track your GUI changes with diff assertions
|
|
155
160
|
test_files: []
|
|
@@ -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
|