snap_diff-capybara 2.0.0.beta3 → 2.0.0.beta4
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 +438 -0
- data/README.md +330 -0
- data/docs/UPGRADING.md +232 -34
- data/docs/architecture.md +9 -7
- data/docs/ci-integration.md +42 -11
- data/docs/configuration.md +176 -10
- data/docs/drivers.md +48 -5
- data/docs/framework-setup.md +10 -3
- data/docs/migration-guide.md +24 -13
- data/docs/reporters.md +70 -1
- data/docs/snapdiff.md +41 -8
- data/docs/thread_safety.md +77 -86
- data/lib/capybara/screenshot/diff/annotation_service.rb +1 -3
- data/lib/capybara/screenshot/diff/area_calculator.rb +1 -3
- data/lib/capybara/screenshot/diff/browser_helpers.rb +1 -3
- data/lib/capybara/screenshot/diff/config_legacy.rb +15 -60
- data/lib/capybara/screenshot/diff/difference.rb +1 -4
- data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +3 -3
- data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +2 -3
- data/lib/capybara/screenshot/diff/drivers.rb +4 -5
- data/lib/capybara/screenshot/diff/image_compare.rb +11 -22
- data/lib/capybara/screenshot/diff/image_preprocessor.rb +1 -3
- data/lib/capybara/screenshot/diff/os.rb +4 -11
- data/lib/capybara/screenshot/diff/region.rb +2 -2
- data/lib/capybara/screenshot/diff/reporters/default.rb +4 -17
- data/lib/capybara/screenshot/diff/screenshot_matcher.rb +1 -3
- data/lib/capybara/screenshot/diff/screenshoter.rb +1 -3
- data/lib/capybara/screenshot/diff/stable_screenshoter.rb +1 -3
- data/lib/capybara/screenshot/diff/utils.rb +1 -3
- data/lib/capybara/screenshot/diff/vcs.rb +1 -3
- data/lib/capybara/screenshot/diff/version.rb +8 -13
- data/lib/capybara-screenshot-diff.rb +10 -1
- data/lib/capybara_screenshot_diff/attempts_reporter.rb +1 -3
- data/lib/capybara_screenshot_diff/dsl.rb +5 -0
- data/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb +1 -4
- data/lib/capybara_screenshot_diff/reporters/html.rb +1 -3
- data/lib/capybara_screenshot_diff/screenshot_namer.rb +1 -3
- data/lib/capybara_screenshot_diff/snap.rb +1 -3
- data/lib/capybara_screenshot_diff/snap_manager.rb +1 -3
- data/lib/capybara_screenshot_diff.rb +17 -21
- data/lib/snap_diff/browser_helpers.rb +26 -5
- data/lib/snap_diff/capture/viewport.rb +2 -5
- data/lib/snap_diff/comparison.rb +54 -3
- data/lib/snap_diff/comparison_result.rb +3 -1
- data/lib/snap_diff/config.rb +189 -93
- data/lib/snap_diff/deprecation.rb +89 -28
- data/lib/snap_diff/driver.rb +18 -0
- data/lib/snap_diff/drivers/vips_driver.rb +12 -6
- data/lib/snap_diff/drivers.rb +94 -12
- data/lib/snap_diff/dsl.rb +45 -47
- data/lib/snap_diff/integrations/cucumber.rb +1 -1
- data/lib/snap_diff/integrations/minitest.rb +45 -9
- data/lib/snap_diff/integrations/rspec.rb +11 -0
- data/lib/snap_diff/legacy_shims.rb +285 -27
- data/lib/snap_diff/removal.rb +159 -0
- data/lib/snap_diff/reporters/default.rb +86 -23
- data/lib/snap_diff/reporters/html.rb +29 -13
- data/lib/snap_diff/reporting.rb +329 -3
- data/lib/snap_diff/screenshot_assertion.rb +28 -23
- data/lib/snap_diff/screenshot_matcher.rb +121 -14
- data/lib/snap_diff/screenshot_namer.rb +1 -19
- data/lib/snap_diff/screenshoter.rb +5 -7
- data/lib/snap_diff/snap.rb +6 -1
- data/lib/snap_diff/snap_manager.rb +2 -3
- data/lib/snap_diff/stable_screenshoter.rb +2 -2
- data/lib/snap_diff/static.rb +1 -1
- data/lib/snap_diff/utils.rb +35 -17
- data/lib/snap_diff/vcs.rb +40 -7
- data/lib/snap_diff/version.rb +1 -1
- data/lib/snap_diff-capybara.rb +33 -3
- data/lib/snap_diff.rb +27 -37
- metadata +12 -10
- data/CODE_OF_CONDUCT.md +0 -129
- data/Rakefile +0 -65
- data/capybara-screenshot-diff.gemspec +0 -29
- data/docs/RELEASE_PREP.md +0 -44
- data/docs/docker-testing.md +0 -24
- data/gems.rb +0 -39
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
# SnapDiff.silence_deprecations? -- the one switch that silences BOTH halves
|
|
4
|
+
# of the story -- lives in snap_diff/removal.rb, not here: the other half
|
|
5
|
+
# (the driver features 2.1 removes) is announced from core files that outlive
|
|
6
|
+
# this one, and they cannot depend on a file the same deletion removes.
|
|
7
|
+
require "snap_diff/removal"
|
|
8
|
+
|
|
3
9
|
module SnapDiff
|
|
4
10
|
# @api private
|
|
5
11
|
#
|
|
@@ -25,26 +31,98 @@ module SnapDiff
|
|
|
25
31
|
# caller who has customized +Warning+ behavior.
|
|
26
32
|
MUTEX = Mutex.new
|
|
27
33
|
@seen = {}
|
|
34
|
+
@notified = false
|
|
35
|
+
@notice_suppressed = false
|
|
36
|
+
@canonical_entry = false
|
|
37
|
+
|
|
38
|
+
# The ONE line a v1 user gets, whichever door they came through. Most of
|
|
39
|
+
# the v1 surface cannot warn per use -- the config accessors are plain
|
|
40
|
+
# delegators and the eager aliases never reach const_missing -- so
|
|
41
|
+
# without this a 2.x app is completely silent right up to the bare
|
|
42
|
+
# NameError it gets on 2.1. Deliberately generic and once per process:
|
|
43
|
+
# an actionable signal, not per-call stderr noise.
|
|
44
|
+
MIGRATION_NOTICE =
|
|
45
|
+
"[snap_diff deprecation] This process uses the v1 `Capybara::Screenshot*` / " \
|
|
46
|
+
"`CapybaraScreenshotDiff*` API. It still works in 2.0 and is REMOVED in 2.1 -- " \
|
|
47
|
+
"see docs/UPGRADING.md for the SnapDiff replacements. Silence with " \
|
|
48
|
+
"`SnapDiff.silence_deprecations = true` or SNAP_DIFF_SILENCE_DEPRECATIONS=1. " \
|
|
49
|
+
"(shown once per process)"
|
|
28
50
|
|
|
29
51
|
class << self
|
|
52
|
+
# Emit {MIGRATION_NOTICE}, exactly once per process. Called from every
|
|
53
|
+
# v1 entry that can be hooked: the const_missing shims (via {.warn}),
|
|
54
|
+
# the generated legacy config accessors, and `include
|
|
55
|
+
# Capybara::Screenshot[::Diff]`.
|
|
56
|
+
# @return [void]
|
|
57
|
+
def notice
|
|
58
|
+
return if @notified || @notice_suppressed || SnapDiff.silence_deprecations?
|
|
59
|
+
|
|
60
|
+
first_time = MUTEX.synchronize { @notified ? false : (@notified = true) }
|
|
61
|
+
Kernel.warn(MIGRATION_NOTICE) if first_time
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# @api private
|
|
65
|
+
#
|
|
66
|
+
# Called at the TOP of every v1-NAMED entry file, before that file's
|
|
67
|
+
# own requires. Requiring one of those paths IS use of the v1 API,
|
|
68
|
+
# and it is the ONE door a suite that merely calls `screenshot` goes
|
|
69
|
+
# through: every other door ({LEGACY_DOORS} in the probe test) needs
|
|
70
|
+
# the user to call something, which is how beta3 shipped a v1-only
|
|
71
|
+
# app that produced zero deprecation output.
|
|
72
|
+
#
|
|
73
|
+
# Position matters. The marker must run before the file's requires,
|
|
74
|
+
# because lib/capybara-screenshot-diff.rb reaches the canonical entry
|
|
75
|
+
# point below on its way in, and a marker placed after that require
|
|
76
|
+
# would be swallowed by {canonical_entry_point!}.
|
|
77
|
+
# @return [void]
|
|
78
|
+
def legacy_entry_point!
|
|
79
|
+
notice unless @canonical_entry
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# @api private
|
|
83
|
+
#
|
|
84
|
+
# Claimed by lib/snap_diff-capybara.rb -- the canonical gem-name
|
|
85
|
+
# entry -- which loads the v1 umbrella itself. Without this, "the v1
|
|
86
|
+
# files got loaded" would be indistinguishable from "a v1 user", and
|
|
87
|
+
# every `gem "snap_diff-capybara"` app would be told to migrate off
|
|
88
|
+
# an API it never touched.
|
|
89
|
+
# @return [void]
|
|
90
|
+
def canonical_entry_point!
|
|
91
|
+
@canonical_entry = true
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @api private
|
|
95
|
+
#
|
|
96
|
+
# Suppresses {MIGRATION_NOTICE} for the rest of the process, without
|
|
97
|
+
# touching the per-constant warnings. For hosts that ARE the v1
|
|
98
|
+
# surface rather than users of it -- this gem's own test suite, which
|
|
99
|
+
# configures through `Capybara::Screenshot.*` by design and would
|
|
100
|
+
# otherwise print the notice on every run. Deliberately survives
|
|
101
|
+
# {reset!}, which exists to give a single test a clean slate.
|
|
102
|
+
# @return [void]
|
|
103
|
+
def suppress_migration_notice!
|
|
104
|
+
MUTEX.synchronize { @notice_suppressed = true }
|
|
105
|
+
end
|
|
106
|
+
|
|
30
107
|
# Emit a deprecation warning for +subject+, exactly once per unique
|
|
31
|
-
# +subject+ per process
|
|
108
|
+
# +subject+ per process -- preceded, the first time round, by
|
|
109
|
+
# {MIGRATION_NOTICE}.
|
|
32
110
|
#
|
|
33
111
|
# @param subject [String] the deprecated old-namespace name being
|
|
34
112
|
# referenced, e.g. "Capybara::Screenshot::Diff::ImageCompare"
|
|
35
113
|
# @param replacement [String] the new-namespace name to use instead
|
|
36
|
-
# @param category [Symbol] what kind of thing moved, for the human
|
|
37
|
-
# reading the message (e.g. :constant, :config)
|
|
38
114
|
# @return [void]
|
|
39
|
-
def warn(subject, replacement
|
|
115
|
+
def warn(subject, replacement)
|
|
40
116
|
return if SnapDiff.silence_deprecations?
|
|
41
117
|
|
|
118
|
+
notice
|
|
119
|
+
|
|
42
120
|
first_time = MUTEX.synchronize do
|
|
43
121
|
@seen.key?(subject) ? false : (@seen[subject] = true)
|
|
44
122
|
end
|
|
45
123
|
return unless first_time
|
|
46
124
|
|
|
47
|
-
Kernel.warn(message_for(subject, replacement,
|
|
125
|
+
Kernel.warn(message_for(subject, replacement, caller_locations(1)))
|
|
48
126
|
end
|
|
49
127
|
|
|
50
128
|
# @api private
|
|
@@ -54,13 +132,16 @@ module SnapDiff
|
|
|
54
132
|
# the suite.
|
|
55
133
|
# @return [void]
|
|
56
134
|
def reset!
|
|
57
|
-
MUTEX.synchronize
|
|
135
|
+
MUTEX.synchronize do
|
|
136
|
+
@seen.clear
|
|
137
|
+
@notified = false
|
|
138
|
+
end
|
|
58
139
|
end
|
|
59
140
|
|
|
60
141
|
private
|
|
61
142
|
|
|
62
|
-
def message_for(subject, replacement,
|
|
63
|
-
message = "[snap_diff deprecation] `#{subject}` is deprecated (
|
|
143
|
+
def message_for(subject, replacement, locations)
|
|
144
|
+
message = "[snap_diff deprecation] `#{subject}` is deprecated (constant); " \
|
|
64
145
|
"use `#{replacement}` instead."
|
|
65
146
|
origin = origin_for(locations)
|
|
66
147
|
origin ? "#{message} (called from #{origin})" : message
|
|
@@ -79,24 +160,4 @@ module SnapDiff
|
|
|
79
160
|
end
|
|
80
161
|
end
|
|
81
162
|
end
|
|
82
|
-
|
|
83
|
-
class << self
|
|
84
|
-
# @api private
|
|
85
|
-
attr_accessor :silence_deprecations
|
|
86
|
-
|
|
87
|
-
# @api private
|
|
88
|
-
#
|
|
89
|
-
# @return [Boolean] true if deprecation warnings should be suppressed,
|
|
90
|
-
# either via the {silence_deprecations} accessor or the
|
|
91
|
-
# SNAP_DIFF_SILENCE_DEPRECATIONS env var (truthy = "1"/"true").
|
|
92
|
-
def silence_deprecations?
|
|
93
|
-
!!silence_deprecations || truthy_env?(ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"])
|
|
94
|
-
end
|
|
95
|
-
|
|
96
|
-
private
|
|
97
|
-
|
|
98
|
-
def truthy_env?(value)
|
|
99
|
-
%w[1 true].include?(value.to_s.downcase)
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
163
|
end
|
data/lib/snap_diff/driver.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "snap_diff/removal"
|
|
4
|
+
|
|
3
5
|
module SnapDiff
|
|
4
6
|
# Shared default behavior for image-processing drivers.
|
|
5
7
|
#
|
|
@@ -10,6 +12,22 @@ module SnapDiff
|
|
|
10
12
|
module Driver
|
|
11
13
|
PNG_EXTENSION = ".png"
|
|
12
14
|
|
|
15
|
+
# Including this mixin is what makes a custom driver a driver, so it is
|
|
16
|
+
# where a custom-driver author can be told that 2.1 removes the whole
|
|
17
|
+
# abstraction. Scoped to drivers that are NOT the gem's own two: both
|
|
18
|
+
# bundled drivers include it themselves, and warning there would fire on
|
|
19
|
+
# every plain vips setup -- about code the user does not own.
|
|
20
|
+
def self.included(base)
|
|
21
|
+
return if base.name.to_s.start_with?("SnapDiff::")
|
|
22
|
+
|
|
23
|
+
Removal.warn_once(
|
|
24
|
+
:driver_mixin,
|
|
25
|
+
"`include SnapDiff::Driver` (in #{base.name || base.inspect}) is REMOVED in 2.1: the " \
|
|
26
|
+
"driver abstraction goes away and libvips becomes the only backend, so custom drivers " \
|
|
27
|
+
"stop working. There is no replacement -- see docs/drivers.md."
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
13
31
|
def same_dimension?(comparison)
|
|
14
32
|
dimension(comparison.base_image) == dimension(comparison.new_image)
|
|
15
33
|
end
|
|
@@ -18,6 +18,17 @@ module SnapDiff
|
|
|
18
18
|
class VipsDriver
|
|
19
19
|
include SnapDiff::Driver
|
|
20
20
|
|
|
21
|
+
# libvips caches loader operations keyed on filename + mtime, and mtime
|
|
22
|
+
# has ONE-SECOND resolution -- so overwriting a path and re-reading it
|
|
23
|
+
# within the same second hands back the PREVIOUS image. This gem does
|
|
24
|
+
# exactly that: the screenshoter writes `<name>.png`,
|
|
25
|
+
# `checkout_base_screenshot` writes `<name>.base.png` from VCS, and the
|
|
26
|
+
# comparison then reads both.
|
|
27
|
+
#
|
|
28
|
+
# `revalidate: true` tells the loader to skip the cached result (libvips
|
|
29
|
+
# 8.15+).
|
|
30
|
+
REVALIDATE = Vips.at_least_libvips?(8, 15) ? {revalidate: true}.freeze : {}.freeze
|
|
31
|
+
|
|
21
32
|
def find_difference_region(comparison)
|
|
22
33
|
new_image, base_image, options = comparison.new_image, comparison.base_image, comparison.options
|
|
23
34
|
|
|
@@ -86,7 +97,7 @@ module SnapDiff
|
|
|
86
97
|
end
|
|
87
98
|
|
|
88
99
|
def from_file(filename)
|
|
89
|
-
result = ::Vips::Image.new_from_file(filename.to_s)
|
|
100
|
+
result = ::Vips::Image.new_from_file(filename.to_s, **REVALIDATE)
|
|
90
101
|
|
|
91
102
|
result = result.colourspace(:srgb) if result.bands < 3
|
|
92
103
|
result = result.bandjoin(255) if result.bands == 3
|
|
@@ -115,11 +126,6 @@ module SnapDiff
|
|
|
115
126
|
private
|
|
116
127
|
|
|
117
128
|
class << self
|
|
118
|
-
def difference_area(old_image, new_image, color_distance: 0)
|
|
119
|
-
mask = difference_mask(new_image, old_image, color_distance)
|
|
120
|
-
difference_area_size_by(mask)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
129
|
def difference_area_size_by(difference_mask)
|
|
124
130
|
diff_mask = difference_mask == 0
|
|
125
131
|
diff_mask.hist_find.to_a[0][0].max
|
data/lib/snap_diff/drivers.rb
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "snap_diff/removal"
|
|
4
|
+
|
|
3
5
|
module SnapDiff
|
|
6
|
+
# utils.rb requires THIS file at its top. Requiring it back at load time
|
|
7
|
+
# made Ruby shout "circular require considered harmful" under $VERBOSE --
|
|
8
|
+
# which Rake::TestTask sets by default, i.e. every standard Rails/Minitest
|
|
9
|
+
# suite. Autoload breaks the cycle without narrowing the surface: nothing
|
|
10
|
+
# here touches Utils until a method runs, and requiring this file still
|
|
11
|
+
# leaves SnapDiff::Utils resolvable exactly as the eager require did.
|
|
12
|
+
autoload :Utils, "snap_diff/utils"
|
|
13
|
+
|
|
4
14
|
# Compare two images and determine if they are equal, different, or within some comparison
|
|
5
15
|
# range considering color values and difference area size.
|
|
6
16
|
module Drivers
|
|
@@ -11,24 +21,96 @@ module SnapDiff
|
|
|
11
21
|
Utils.find_driver_class_for(driver_option).new
|
|
12
22
|
end
|
|
13
23
|
|
|
24
|
+
# @api private
|
|
25
|
+
#
|
|
26
|
+
# The registry itself, unannounced: driver name => driver class, filled
|
|
27
|
+
# lazily by Utils.find_driver_class_for and mutated in place. The gem's
|
|
28
|
+
# own reads go through HERE rather than through .loaded, so the removal
|
|
29
|
+
# warning below stays a signal about USER code -- a gem that warns at
|
|
30
|
+
# itself teaches people to ignore its warnings.
|
|
31
|
+
def self.registry
|
|
32
|
+
@registry ||= {}
|
|
33
|
+
end
|
|
34
|
+
|
|
14
35
|
# Canonical driver-class cache (ADR-008 step 5b, ex
|
|
15
36
|
# Capybara::Screenshot::Diff::LOADED_DRIVERS): driver name => driver
|
|
16
|
-
# class
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
37
|
+
# class. Mutated in place -- including by user registration through the
|
|
38
|
+
# legacy constant, which legacy_shims pins as an EAGER same-object alias
|
|
39
|
+
# of this hash (a lazy copy would silently drop such registrations).
|
|
40
|
+
#
|
|
41
|
+
# THE documented custom-driver registration point (docs/snapdiff.md), so
|
|
42
|
+
# a custom-driver author has to hear that 2.1 takes it away.
|
|
20
43
|
def self.loaded
|
|
21
|
-
|
|
44
|
+
Removal.warn_once(
|
|
45
|
+
:drivers_loaded,
|
|
46
|
+
"`SnapDiff::Drivers.loaded` is REMOVED in 2.1 together with the rest of the driver " \
|
|
47
|
+
"abstraction (`SnapDiff::Driver`, `SnapDiff::Drivers.available`, `driver: :auto`): " \
|
|
48
|
+
"libvips becomes the only backend and custom drivers are no longer supported. " \
|
|
49
|
+
"See docs/drivers.md."
|
|
50
|
+
)
|
|
51
|
+
registry
|
|
22
52
|
end
|
|
23
53
|
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
54
|
+
# Which image drivers this process can actually load, in preference
|
|
55
|
+
# order. Tries the gem first and falls back to `require`, cleaning up
|
|
56
|
+
# the half-defined constant a failed native load leaves behind.
|
|
57
|
+
def self.detect_available
|
|
58
|
+
result = []
|
|
59
|
+
begin
|
|
60
|
+
result << :vips if defined?(Vips) || require("vips")
|
|
61
|
+
rescue LoadError
|
|
62
|
+
# vips not present
|
|
63
|
+
Object.send(:remove_const, :Vips) if defined?(Vips)
|
|
64
|
+
end
|
|
65
|
+
begin
|
|
66
|
+
result << :chunky_png if defined?(ChunkyPNG) || require("chunky_png")
|
|
67
|
+
rescue LoadError
|
|
68
|
+
# chunky_png not present
|
|
69
|
+
Object.send(:remove_const, :ChunkyPNG) if defined?(ChunkyPNG)
|
|
70
|
+
end
|
|
71
|
+
result
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Canonical home of the detected-drivers list (3.0 readiness: it used
|
|
75
|
+
# to live only on Capybara::Screenshot::Diff::AVAILABLE_DRIVERS, so
|
|
76
|
+
# `require "snap_diff/drivers"` alone left .available raising
|
|
77
|
+
# NameError). Detection runs HERE, at this file's load, and the legacy
|
|
78
|
+
# constant is now an eager same-object alias of this one.
|
|
79
|
+
AVAILABLE_DRIVERS = detect_available.freeze
|
|
80
|
+
|
|
81
|
+
# The driver classes are documented names (the legacy
|
|
82
|
+
# `...::Drivers::VipsDriver` path is a same-object alias of this one), so
|
|
83
|
+
# naming one has to work without a prior require. Autoload rather than
|
|
84
|
+
# require: naming one must not cost every process the vips/chunky_png
|
|
85
|
+
# load, and Utils.find_driver_class_for still requires them explicitly.
|
|
86
|
+
#
|
|
87
|
+
# Gated on AVAILABLE_DRIVERS, which is why these sit BELOW it. Declaring
|
|
88
|
+
# them unconditionally made `const_defined?(:VipsDriver)` true on a box
|
|
89
|
+
# without ruby-vips (neither driver gem is a runtime dependency), so the
|
|
90
|
+
# documented v1 pattern `Diff.driver = :vips if defined?(...VipsDriver)`
|
|
91
|
+
# took the branch and then blew up on const_get. v1.12.0 loaded
|
|
92
|
+
# vips_driver.rb only from find_driver_class_for, so `defined?` was nil
|
|
93
|
+
# there -- this keeps that.
|
|
94
|
+
autoload :ChunkyPNGDriver, "snap_diff/drivers/chunky_png_driver" if AVAILABLE_DRIVERS.include?(:chunky_png)
|
|
95
|
+
autoload :VipsDriver, "snap_diff/drivers/vips_driver" if AVAILABLE_DRIVERS.include?(:vips)
|
|
96
|
+
|
|
97
|
+
# Canonical read API for the list above. Reads the constant live rather
|
|
98
|
+
# than caching, because the constant is the published stubbing point
|
|
99
|
+
# (image_compare_test stubs it to [] to exercise the no-drivers error
|
|
100
|
+
# path).
|
|
101
|
+
#
|
|
102
|
+
# Detection only exists because there is a choice of backend to detect;
|
|
103
|
+
# 2.1 removes the choice, so it warns. The gem's own callers read
|
|
104
|
+
# AVAILABLE_DRIVERS directly -- same value, same stubbing point, no
|
|
105
|
+
# warning at itself.
|
|
30
106
|
def self.available
|
|
31
|
-
|
|
107
|
+
Removal.warn_once(
|
|
108
|
+
:drivers_available,
|
|
109
|
+
"`SnapDiff::Drivers.available` is REMOVED in 2.1: with libvips the only backend there " \
|
|
110
|
+
"is nothing left to detect. Require the `ruby-vips` gem instead of branching on this " \
|
|
111
|
+
"list. See docs/drivers.md."
|
|
112
|
+
)
|
|
113
|
+
AVAILABLE_DRIVERS
|
|
32
114
|
end
|
|
33
115
|
end
|
|
34
116
|
end
|
data/lib/snap_diff/dsl.rb
CHANGED
|
@@ -4,23 +4,21 @@
|
|
|
4
4
|
# snap_diff/ (this file, snap_diff/integrations/*, snap_diff/static) routes
|
|
5
5
|
# through here, so this one line is what makes SnapDiff.configure/.start/
|
|
6
6
|
# .compare/::VERSION -- and the dual-install guard -- present no matter
|
|
7
|
-
# which of those paths the user picked.
|
|
8
|
-
# file back, so the require graph stays acyclic.
|
|
7
|
+
# which of those paths the user picked.
|
|
9
8
|
require "snap_diff"
|
|
10
9
|
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# at
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# old-path forwarder).
|
|
10
|
+
# Must NOT require "capybara_screenshot_diff": that would cycle back here via
|
|
11
|
+
# this file's old-path forwarder. Nothing from the v1 trees is required here
|
|
12
|
+
# at all (3.0 readiness): the three requires below used to point at their
|
|
13
|
+
# capybara/screenshot/diff/* forwarders, which made this unit depend on the
|
|
14
|
+
# compatibility tree it is meant to replace.
|
|
17
15
|
# DSL includes Capybara::DSL directly below, so it needs the base gem
|
|
18
16
|
# loaded regardless of what pulled this file in.
|
|
19
17
|
require "capybara/dsl"
|
|
20
|
-
require "
|
|
21
|
-
require "
|
|
18
|
+
require "snap_diff/config"
|
|
19
|
+
require "snap_diff/drivers"
|
|
22
20
|
require "snap_diff/comparison"
|
|
23
|
-
require "
|
|
21
|
+
require "snap_diff/screenshot_matcher"
|
|
24
22
|
require_relative "screenshot_namer"
|
|
25
23
|
require_relative "screenshot_assertion"
|
|
26
24
|
|
|
@@ -35,11 +33,11 @@ module SnapDiff
|
|
|
35
33
|
include Capybara::DSL
|
|
36
34
|
|
|
37
35
|
def screenshot_section(name)
|
|
38
|
-
screenshot_namer.section = name
|
|
36
|
+
SnapDiff.session.screenshot_namer.section = name
|
|
39
37
|
end
|
|
40
38
|
|
|
41
39
|
def screenshot_group(name)
|
|
42
|
-
screenshot_namer.group = name
|
|
40
|
+
SnapDiff.session.screenshot_namer.group = name
|
|
43
41
|
end
|
|
44
42
|
|
|
45
43
|
# Takes a screenshot and compares it against a baseline image.
|
|
@@ -53,7 +51,7 @@ module SnapDiff
|
|
|
53
51
|
# @param name [String] The base name of the screenshot, used to generate the filename.
|
|
54
52
|
# @param skip_stack_frames [Integer] The number of stack frames to skip when reporting errors.
|
|
55
53
|
# @param options [Hash] Additional options for taking the screenshot and comparison.
|
|
56
|
-
# @option options [Boolean] :delayed (
|
|
54
|
+
# @option options [Boolean] :delayed (SnapDiff.config.delayed)
|
|
57
55
|
# Whether to validate the screenshot immediately or delay validation.
|
|
58
56
|
# @option options [Array<Integer>] :crop [left, top, right, bottom] Edge coordinates to crop the screenshot to.
|
|
59
57
|
# @option options [Array<Array<Integer>>] :skip_area Array of [left, top, right, bottom] edge coordinates to ignore.
|
|
@@ -66,23 +64,40 @@ module SnapDiff
|
|
|
66
64
|
# @option options [Numeric] :shift_distance_limit Maximum allowed shift distance for pixels.
|
|
67
65
|
# @option options [Numeric] :area_size_limit Maximum allowed difference area size in pixels.
|
|
68
66
|
# @option options [Symbol] :driver (:auto) The image processing driver to use (:auto, :chunky_png, :vips).
|
|
67
|
+
# @yield Optional readiness block: work that must happen before the page
|
|
68
|
+
# is captured -- settling lazy-loaded images, `document.fonts.ready`,
|
|
69
|
+
# waiting on a widget. Runs AFTER the `active?` guard and before the
|
|
70
|
+
# capture, exactly once per assertion (not once per stability
|
|
71
|
+
# attempt), and an error raised inside it propagates unchanged.
|
|
72
|
+
#
|
|
73
|
+
# The point is not ergonomics -- while screenshots are on, the block
|
|
74
|
+
# does nothing a line above the call could not. The point is what
|
|
75
|
+
# happens when they are OFF: this method returns at the guard above,
|
|
76
|
+
# so a preceding `preload_all_images` still costs its browser
|
|
77
|
+
# round-trips while the block costs nothing. Readiness work belongs
|
|
78
|
+
# inside the same switch as the capture it serves.
|
|
69
79
|
# @return [Boolean] True if the screenshot was successfully captured and processed.
|
|
70
80
|
# @raise [SnapDiff::ExpectationNotMet] If comparison fails and immediate validation is enabled.
|
|
71
81
|
# @raise [SnapDiff::UnstableImage] If the image comparison is unstable.
|
|
72
82
|
# @raise [SnapDiff::WindowSizeMismatchError] If the window size doesn't match expectations.
|
|
73
83
|
def assert_matches_screenshot(name, skip_stack_frames: 0, **options)
|
|
74
|
-
return false unless
|
|
84
|
+
return false unless SnapDiff.config.active?
|
|
85
|
+
|
|
86
|
+
yield if block_given?
|
|
75
87
|
|
|
76
88
|
# Get the full name with section and group information
|
|
77
89
|
full_name = SnapDiff.session.screenshot_namer.full_name(name)
|
|
78
90
|
|
|
79
|
-
# Build the screenshot assertion
|
|
80
|
-
|
|
91
|
+
# Build the screenshot assertion; the actual comparison is deferred
|
|
92
|
+
# until ScreenshotAssertion#validate! runs.
|
|
93
|
+
assertion = SnapDiff::ScreenshotMatcher
|
|
94
|
+
.new(full_name, options)
|
|
95
|
+
.build_screenshot_assertion(skip_stack_frames: skip_stack_frames + 1)
|
|
81
96
|
|
|
82
97
|
return false unless assertion
|
|
83
98
|
|
|
84
99
|
# Determine if validation should be delayed or immediate
|
|
85
|
-
delayed = options.fetch(:delayed,
|
|
100
|
+
delayed = options.fetch(:delayed, SnapDiff.config.delayed)
|
|
86
101
|
|
|
87
102
|
if delayed
|
|
88
103
|
SnapDiff.session.add_assertion(assertion)
|
|
@@ -95,22 +110,28 @@ module SnapDiff
|
|
|
95
110
|
|
|
96
111
|
# Convenience wrapper around {#assert_matches_screenshot} and {#capture_screenshot}.
|
|
97
112
|
# @param compare [Boolean] When false, only captures the screenshot without comparing it to a baseline.
|
|
113
|
+
# @yield Forwarded to whichever of the two it delegates to. A delegator
|
|
114
|
+
# that swallowed the block would give the user a readiness block that
|
|
115
|
+
# silently never runs.
|
|
98
116
|
# @see #assert_matches_screenshot
|
|
99
117
|
# @see #capture_screenshot
|
|
100
|
-
def screenshot(name, skip_stack_frames: 0, compare: true, **options)
|
|
118
|
+
def screenshot(name, skip_stack_frames: 0, compare: true, **options, &readiness)
|
|
101
119
|
if compare
|
|
102
|
-
assert_matches_screenshot(name, skip_stack_frames: skip_stack_frames + 1, **options)
|
|
120
|
+
assert_matches_screenshot(name, skip_stack_frames: skip_stack_frames + 1, **options, &readiness)
|
|
103
121
|
else
|
|
104
|
-
capture_screenshot(name, **options)
|
|
122
|
+
capture_screenshot(name, **options, &readiness)
|
|
105
123
|
end
|
|
106
124
|
end
|
|
107
125
|
|
|
108
126
|
# Captures a screenshot without comparing it to a baseline.
|
|
109
127
|
# @param name [String] The base name of the screenshot, used to generate the filename.
|
|
110
128
|
# @param options [Hash] Additional options for taking the screenshot. See {#assert_matches_screenshot}.
|
|
129
|
+
# @yield Optional readiness block. See {#assert_matches_screenshot}.
|
|
111
130
|
# @return [Boolean] True if the screenshot was successfully captured.
|
|
112
131
|
def capture_screenshot(name, **options)
|
|
113
|
-
return false unless
|
|
132
|
+
return false unless SnapDiff.config.active?
|
|
133
|
+
|
|
134
|
+
yield if block_given?
|
|
114
135
|
|
|
115
136
|
full_name = SnapDiff.session.screenshot_namer.full_name(name)
|
|
116
137
|
SnapDiff::ScreenshotMatcher.new(full_name, options).capture
|
|
@@ -121,31 +142,8 @@ module SnapDiff
|
|
|
121
142
|
# Asserts the current page has no visual changes from the baseline.
|
|
122
143
|
# Override in your base test class to add project-specific behavior
|
|
123
144
|
# (e.g., waiting for Turbo, default skip areas).
|
|
124
|
-
def assert_no_screenshot_changes(name, skip_stack_frames: 0, **opts)
|
|
125
|
-
assert_matches_screenshot(name, skip_stack_frames: skip_stack_frames + 1, **opts)
|
|
126
|
-
end
|
|
127
|
-
|
|
128
|
-
private
|
|
129
|
-
|
|
130
|
-
# Builds a screenshot assertion object that can be validated immediately or later.
|
|
131
|
-
#
|
|
132
|
-
# This method constructs a screenshot assertion that encapsulates the comparison logic.
|
|
133
|
-
# The actual comparison is deferred until {ScreenshotAssertion#validate!} is called.
|
|
134
|
-
#
|
|
135
|
-
# @param name [String] The full name of the screenshot, including any section/group context.
|
|
136
|
-
# @param options [Hash] Options for screenshot taking and comparison.
|
|
137
|
-
# See {#assert_matches_screenshot} for available options.
|
|
138
|
-
# @param skip_stack_frames [Integer] Number of stack frames to skip for error reporting.
|
|
139
|
-
# @return [ScreenshotAssertion, nil] The assertion object or nil if no assertion is needed.
|
|
140
|
-
# @see ScreenshotAssertion
|
|
141
|
-
def build_screenshot_assertion(name, options, skip_stack_frames: 0)
|
|
142
|
-
SnapDiff::ScreenshotMatcher
|
|
143
|
-
.new(name, options)
|
|
144
|
-
.build_screenshot_assertion(skip_stack_frames: skip_stack_frames + 1)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def screenshot_namer
|
|
148
|
-
SnapDiff.session.screenshot_namer
|
|
145
|
+
def assert_no_screenshot_changes(name, skip_stack_frames: 0, **opts, &readiness)
|
|
146
|
+
assert_matches_screenshot(name, skip_stack_frames: skip_stack_frames + 1, **opts, &readiness)
|
|
149
147
|
end
|
|
150
148
|
end
|
|
151
149
|
end
|
|
@@ -8,14 +8,21 @@ require_relative "../dsl"
|
|
|
8
8
|
require "snap_diff/screenshot_assertion"
|
|
9
9
|
require "snap_diff/reporting"
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
# Only the v1 NAMESPACE entry is a deprecated choice: requiring
|
|
12
|
+
# "capybara/screenshot/diff" is a line in the user's own file, and changing
|
|
13
|
+
# it is the fix. The gem-NAME file (lib/capybara-screenshot-diff.rb) is not:
|
|
14
|
+
# `Bundler.require` requires the gem's own name, so it loads for everyone
|
|
15
|
+
# with the gem in their Gemfile whatever they require explicitly -- keying
|
|
16
|
+
# the warning off it shouted at every user on every run, with no action
|
|
17
|
+
# available to silence it. See test/legacy/minitest_activation_warning_test.rb.
|
|
18
|
+
#
|
|
19
|
+
# Silenceable through the documented switch (SnapDiff.silence_deprecations /
|
|
20
|
+
# SNAP_DIFF_SILENCE_DEPRECATIONS): it was a bare Kernel#warn, so the one knob
|
|
21
|
+
# the docs offer did not reach it.
|
|
22
|
+
if !SnapDiff.silence_deprecations? && caller.any? { |path| path.include?("capybara/screenshot/diff.rb") }
|
|
16
23
|
warn <<~MSG
|
|
17
|
-
[DEPRECATION]
|
|
18
|
-
Please `require "
|
|
24
|
+
[DEPRECATION] `require "capybara/screenshot/diff"` activates the Minitest assertions for you; that will be removed.
|
|
25
|
+
Please `require "snap_diff/integrations/minitest"` explicitly.
|
|
19
26
|
MSG
|
|
20
27
|
end
|
|
21
28
|
|
|
@@ -24,8 +31,20 @@ module SnapDiff
|
|
|
24
31
|
module Assertions
|
|
25
32
|
include ::SnapDiff::DSL
|
|
26
33
|
|
|
34
|
+
# The `if` is the whole point (issue #270). `super` returns false
|
|
35
|
+
# immediately when screenshots are disabled -- nothing captured,
|
|
36
|
+
# nothing compared -- so counting unconditionally reported
|
|
37
|
+
# `1 runs, 1 assertions, 0 failures` over a test that asserted
|
|
38
|
+
# nothing at all.
|
|
39
|
+
#
|
|
40
|
+
# Getting the count right hands the alarm to Rails for free: it
|
|
41
|
+
# prepends ActiveSupport::Testing::TestsWithoutAssertions into every
|
|
42
|
+
# ActiveSupport::TestCase (test_case.rb:205), which warns
|
|
43
|
+
# "Test is missing assertions: `test_x`" on exactly the tests whose
|
|
44
|
+
# sole assertion was a disabled screenshot -- and stays quiet for
|
|
45
|
+
# tests that assert something else.
|
|
27
46
|
def assert_matches_screenshot(*args, skip_stack_frames: 0, **opts)
|
|
28
|
-
self.assertions += 1
|
|
47
|
+
self.assertions += 1 if SnapDiff.config.active?
|
|
29
48
|
|
|
30
49
|
super(*args, skip_stack_frames: skip_stack_frames + 1, **opts)
|
|
31
50
|
rescue ::SnapDiff::ExpectationNotMet => e
|
|
@@ -66,4 +85,21 @@ module SnapDiff
|
|
|
66
85
|
end
|
|
67
86
|
end
|
|
68
87
|
|
|
69
|
-
|
|
88
|
+
# Under Rails' `parallelize(workers: N)` the tests run in forked children
|
|
89
|
+
# that never reach `after_run`, so the report and the summary line were
|
|
90
|
+
# lost on every suite past the parallelization threshold (issue #258).
|
|
91
|
+
# Registering the worker-side hook has to happen before `parallelize`
|
|
92
|
+
# forks; with `Bundler.require` this file loads before
|
|
93
|
+
# ActiveSupport::TestCase exists, so try now and again when it appears.
|
|
94
|
+
unless SnapDiff::Reporting.install_parallel_hooks!
|
|
95
|
+
if defined?(::ActiveSupport) && ::ActiveSupport.respond_to?(:on_load)
|
|
96
|
+
::ActiveSupport.on_load(:active_support_test_case) { SnapDiff::Reporting.install_parallel_hooks! }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
if ::Minitest.respond_to?(:after_run)
|
|
101
|
+
::Minitest.after_run do
|
|
102
|
+
SnapDiff::Reporting.merge_parallel_fragments!
|
|
103
|
+
SnapDiff::Reporting.finalize!
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -9,6 +9,17 @@ require "snap_diff/reporting"
|
|
|
9
9
|
RSpec::Matchers.define :match_screenshot do |name, **options|
|
|
10
10
|
description { "match screenshot '#{name}'" }
|
|
11
11
|
|
|
12
|
+
# The literal `true` is deliberate, not the Minitest miscount of issue
|
|
13
|
+
# #270. `assert_matches_screenshot` returns false when screenshots are
|
|
14
|
+
# disabled, and returning that here would FAIL the example for a config
|
|
15
|
+
# switch the user set on purpose. A real mismatch does not come back as
|
|
16
|
+
# false either -- it raises SnapDiff::ExpectationNotMet, or is deferred
|
|
17
|
+
# to the append_after hook below.
|
|
18
|
+
#
|
|
19
|
+
# There is nothing to hand off to the way Minitest hands off to Rails'
|
|
20
|
+
# TestsWithoutAssertions: RSpec has no assertion count, so a disabled
|
|
21
|
+
# screenshot leaves an example that passed having checked nothing, and
|
|
22
|
+
# only the end-of-run `0 verified` line can see it.
|
|
12
23
|
match do |_page|
|
|
13
24
|
assert_matches_screenshot(name, **options)
|
|
14
25
|
true
|