snap_diff-capybara 2.0.0.beta2 → 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 +471 -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/cucumber.rb +5 -1
- 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 +12 -11
- 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 -3
- 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/static.rb +4 -0
- 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 +51 -45
- data/lib/snap_diff/errors.rb +7 -1
- 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 +283 -28
- 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 +330 -4
- 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 +38 -0
- data/lib/snap_diff.rb +40 -39
- metadata +13 -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,37 +1,92 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "snap_diff/comparison"
|
|
4
|
+
require "snap_diff/config"
|
|
3
5
|
require "snap_diff/deprecation"
|
|
4
6
|
require "snap_diff/drivers"
|
|
7
|
+
require "snap_diff/errors"
|
|
8
|
+
require "snap_diff/os"
|
|
9
|
+
require "snap_diff/reporters/default"
|
|
10
|
+
require "snap_diff/version"
|
|
5
11
|
|
|
6
|
-
#
|
|
7
|
-
#
|
|
12
|
+
# THE v1 compatibility surface, in one file -- and the whole of it that is
|
|
13
|
+
# code. lib/capybara* is alias-only by contract
|
|
14
|
+
# (legacy_tree_is_alias_only_test.rb) and the canonical core names nothing
|
|
15
|
+
# from it (core_tree_has_no_legacy_deps_test.rb), so this file plus those
|
|
16
|
+
# trees is exactly what 3.0 deletes.
|
|
17
|
+
#
|
|
18
|
+
# Three things live here:
|
|
19
|
+
# 1. the const_missing forwarders for the pre-v2 namespaces (below);
|
|
20
|
+
# 2. CONFIG_MAPPING -- the old mattr_accessor surface, generated as thin
|
|
21
|
+
# delegators onto SnapDiff.config (which owns the storage);
|
|
22
|
+
# 3. the derived/config forwarders that used to sit in config_legacy.rb
|
|
23
|
+
# (Screenshot.active?, Diff.configure, SnapDiff.start, ...).
|
|
24
|
+
#
|
|
25
|
+
# 2 and 3 moved here so `require "snap_diff"` can keep offering the full v1
|
|
26
|
+
# surface -- as it always has -- without the core requiring anything from
|
|
27
|
+
# lib/capybara/.
|
|
28
|
+
#
|
|
29
|
+
# const_missing-based forwarders for the pre-v2 namespaces. Every old-name
|
|
30
|
+
# lookup below resolves -- lazily -- to the exact
|
|
8
31
|
# same object as its SnapDiff:: replacement (identity pinned by
|
|
9
32
|
# test/unit/namespace_forwarding_test.rb) and emits a deprecation warning,
|
|
10
33
|
# once per constant per process, silenceable via
|
|
11
34
|
# SnapDiff.silence_deprecations or SNAP_DIFF_SILENCE_DEPRECATIONS=1.
|
|
12
35
|
#
|
|
13
|
-
# Deliberately eager-and-silent exceptions (plain constants
|
|
14
|
-
#
|
|
36
|
+
# Deliberately eager-and-silent exceptions (plain constants assigned BELOW,
|
|
37
|
+
# never warn individually -- the once-per-process migration notice still
|
|
38
|
+
# fires for the paths that CAN be hooked; see Deprecation.notice):
|
|
15
39
|
#
|
|
16
|
-
# - Capybara::Screenshot::Os
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
40
|
+
# - Capybara::Screenshot::Os: an advertised entry-point constant probed with
|
|
41
|
+
# Object.const_defined? by support_load_probe_test.rb -- const_defined?
|
|
42
|
+
# never triggers const_missing, so a lazy shim would break that contract.
|
|
43
|
+
# - Capybara::Screenshot::Diff::VERSION, and ::Comparison (the images
|
|
44
|
+
# struct): documented user-facing names that adopters feature-detect with
|
|
45
|
+
# defined?/const_defined?.
|
|
46
|
+
# - Capybara::Screenshot::Diff::Reporters::Default (a documented subclassing
|
|
47
|
+
# extension point): same reasoning.
|
|
48
|
+
# - The CapybaraScreenshotDiff error classes: rescue-by-old-name and
|
|
49
|
+
# defined? feature detection must keep behaving exactly as before.
|
|
23
50
|
# - Drivers::ChunkyPNGDriver / Drivers::VipsDriver: real constants on the
|
|
24
51
|
# shared SnapDiff::Drivers module (the Drivers alias is same-object by
|
|
25
52
|
# contract), so const_missing can never fire for the leaf names;
|
|
26
|
-
# resolving them through the old path still warns for ...::Drivers.
|
|
53
|
+
# resolving them through the old path still warns for ...::Drivers. They
|
|
54
|
+
# are `autoload`ed there, so naming one loads it.
|
|
27
55
|
# - Diff::LOADED_DRIVERS: user code registers custom drivers by mutating
|
|
28
56
|
# this hash in place, so it must be the exact same object as the
|
|
29
57
|
# canonical SnapDiff::Drivers.loaded -- a lazy warn-once shim could not
|
|
30
58
|
# keep a mutable alias, and warning on a supported registration surface
|
|
31
59
|
# would be noise. Assigned eagerly below.
|
|
32
|
-
# - Diff::AVAILABLE_DRIVERS: stays a real constant
|
|
33
|
-
#
|
|
34
|
-
# SnapDiff::Drivers.available is the canonical reader
|
|
60
|
+
# - Diff::AVAILABLE_DRIVERS: stays a real constant, aliased BELOW from the
|
|
61
|
+
# canonical SnapDiff::Drivers::AVAILABLE_DRIVERS (same object;
|
|
62
|
+
# SnapDiff::Drivers.available is the canonical reader, and its constant is
|
|
63
|
+
# the stubbing point -- stubbing this alias only rebinds the alias).
|
|
64
|
+
#
|
|
65
|
+
# All of them are assigned HERE rather than in their own forwarder files
|
|
66
|
+
# under lib/capybara*. Those forwarders are loaded only by the LEGACY entry
|
|
67
|
+
# points, so a partially migrated app -- one that swapped its `require` line
|
|
68
|
+
# for a canonical `snap_diff*` one first, exactly as UPGRADING.md tells it
|
|
69
|
+
# to, and has not renamed its constants yet -- lost every one of them and
|
|
70
|
+
# died on `uninitialized constant Capybara::Screenshot::Os`. This file is
|
|
71
|
+
# required by every entry point, canonical and legacy, so it is the only
|
|
72
|
+
# place the eager exceptions can actually be eager.
|
|
73
|
+
#
|
|
74
|
+
# CapybaraScreenshotDiff::DSL and ::Minitest::Assertions are the two that
|
|
75
|
+
# CANNOT be eager here: snap_diff/dsl requires "snap_diff" (which requires
|
|
76
|
+
# this file), and snap_diff/integrations/minitest pulls in the minitest gem,
|
|
77
|
+
# which no canonical entry point should force on a process. They are mapped
|
|
78
|
+
# lazily below instead, and stay eager under the legacy entry points that
|
|
79
|
+
# load their forwarder files.
|
|
80
|
+
|
|
81
|
+
# The v1 namespaces, predefined empty so CONFIG_MAPPING can name them at
|
|
82
|
+
# class-body eval time. Everything below reopens them.
|
|
83
|
+
module Capybara
|
|
84
|
+
module Screenshot
|
|
85
|
+
module Diff
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
35
90
|
module SnapDiff
|
|
36
91
|
# @api private
|
|
37
92
|
module LegacyShims
|
|
@@ -45,20 +100,207 @@ module SnapDiff
|
|
|
45
100
|
target = mapping[name]
|
|
46
101
|
return super(name) unless target
|
|
47
102
|
|
|
48
|
-
Deprecation.warn("#{old_prefix}::#{name}", target
|
|
49
|
-
|
|
103
|
+
Deprecation.warn("#{old_prefix}::#{name}", target)
|
|
104
|
+
LegacyShims.resolve("#{old_prefix}::#{name}", target)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# The handful of replacements whose file name does not follow the gem's
|
|
109
|
+
# own convention (SnapDiff::AreaCalculator -> snap_diff/area_calculator).
|
|
110
|
+
REQUIRE_PATHS = {
|
|
111
|
+
"SnapDiff::Minitest::Assertions" => "snap_diff/integrations/minitest"
|
|
112
|
+
}.freeze
|
|
113
|
+
|
|
114
|
+
# Resolving an old name has to LOAD the replacement, not merely name it.
|
|
115
|
+
# The v1 entry points required the whole gem, so v1 code could say
|
|
116
|
+
# `Capybara::Screenshot::Diff::Utils` with nothing else required; the
|
|
117
|
+
# canonical entry points are lean, so the shim used to resolve its
|
|
118
|
+
# mapping and then die on a bare "uninitialized constant SnapDiff::Utils"
|
|
119
|
+
# -- an internal name the reader has no way to act on.
|
|
120
|
+
def self.resolve(old_name, target)
|
|
121
|
+
require_unit(target) unless Object.const_defined?(target)
|
|
122
|
+
Object.const_get(target)
|
|
123
|
+
rescue NameError
|
|
124
|
+
# Deliberately does NOT advise "reference #{target} directly": we just
|
|
125
|
+
# failed to load it, so that name does not exist either.
|
|
126
|
+
raise NameError, "`#{old_name}` maps to `#{target}`, which this process cannot load. " \
|
|
127
|
+
"See docs/UPGRADING.md for the v1 -> SnapDiff name map."
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def self.require_unit(target)
|
|
131
|
+
require(REQUIRE_PATHS[target] || target
|
|
132
|
+
.gsub("::", "/")
|
|
133
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
134
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
135
|
+
.downcase)
|
|
136
|
+
rescue LoadError
|
|
137
|
+
# No file of its own -- the name lives inside another unit
|
|
138
|
+
# (SnapDiff::RED_RGBA, ::BacktraceFilter). The const_get above decides
|
|
139
|
+
# whether it is already loaded.
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# config attr name => [legacy module, legacy accessor name].
|
|
143
|
+
# The keys are exactly SnapDiff::Config::SETTINGS; this hash only says
|
|
144
|
+
# which of the two legacy holders each one used to hang off, and under
|
|
145
|
+
# what name (only +screenshot_enabled+ differs -- see Config::SETTINGS).
|
|
146
|
+
CONFIG_MAPPING = {
|
|
147
|
+
# Capybara::Screenshot
|
|
148
|
+
add_driver_path: [Capybara::Screenshot, :add_driver_path],
|
|
149
|
+
add_os_path: [Capybara::Screenshot, :add_os_path],
|
|
150
|
+
blur_active_element: [Capybara::Screenshot, :blur_active_element],
|
|
151
|
+
screenshot_enabled: [Capybara::Screenshot, :enabled],
|
|
152
|
+
hide_caret: [Capybara::Screenshot, :hide_caret],
|
|
153
|
+
disable_animations: [Capybara::Screenshot, :disable_animations],
|
|
154
|
+
root: [Capybara::Screenshot, :root],
|
|
155
|
+
stability_time_limit: [Capybara::Screenshot, :stability_time_limit],
|
|
156
|
+
window_size: [Capybara::Screenshot, :window_size],
|
|
157
|
+
save_path: [Capybara::Screenshot, :save_path],
|
|
158
|
+
use_lfs: [Capybara::Screenshot, :use_lfs],
|
|
159
|
+
screenshot_format: [Capybara::Screenshot, :screenshot_format],
|
|
160
|
+
capybara_screenshot_options: [Capybara::Screenshot, :capybara_screenshot_options],
|
|
161
|
+
# Capybara::Screenshot::Diff
|
|
162
|
+
delayed: [Capybara::Screenshot::Diff, :delayed],
|
|
163
|
+
area_size_limit: [Capybara::Screenshot::Diff, :area_size_limit],
|
|
164
|
+
# New in 2.0 and it has no v1 history, but it is storage on the one
|
|
165
|
+
# Config like everything else, and the two-views invariant is
|
|
166
|
+
# all-or-nothing: an unmapped setting is storage the v1 surface cannot
|
|
167
|
+
# see. Mapping it also means a user still on the old namespace reaches
|
|
168
|
+
# the accept workflow without migrating first.
|
|
169
|
+
record: [Capybara::Screenshot::Diff, :record],
|
|
170
|
+
fail_if_new: [Capybara::Screenshot::Diff, :fail_if_new],
|
|
171
|
+
pending_if_new: [Capybara::Screenshot::Diff, :pending_if_new],
|
|
172
|
+
fail_on_difference: [Capybara::Screenshot::Diff, :fail_on_difference],
|
|
173
|
+
color_distance_limit: [Capybara::Screenshot::Diff, :color_distance_limit],
|
|
174
|
+
enabled: [Capybara::Screenshot::Diff, :enabled],
|
|
175
|
+
shift_distance_limit: [Capybara::Screenshot::Diff, :shift_distance_limit],
|
|
176
|
+
skip_area: [Capybara::Screenshot::Diff, :skip_area],
|
|
177
|
+
driver: [Capybara::Screenshot::Diff, :driver],
|
|
178
|
+
tolerance: [Capybara::Screenshot::Diff, :tolerance],
|
|
179
|
+
perceptual_threshold: [Capybara::Screenshot::Diff, :perceptual_threshold],
|
|
180
|
+
screenshoter: [Capybara::Screenshot::Diff, :screenshoter],
|
|
181
|
+
manager: [Capybara::Screenshot::Diff, :manager]
|
|
182
|
+
}.freeze
|
|
183
|
+
|
|
184
|
+
# Installs the old mattr_accessor surface onto the legacy modules,
|
|
185
|
+
# delegating to the single storage in SnapDiff.config. mattr_accessor
|
|
186
|
+
# used to define both singleton and instance accessors (the instance
|
|
187
|
+
# ones are what `include Capybara::Screenshot::Diff` picks up), so both
|
|
188
|
+
# are installed. root keeps its historical asymmetry -- readable
|
|
189
|
+
# everywhere, writable only at module level (it was mattr_reader plus a
|
|
190
|
+
# custom module-level writer) -- with the Pathname coercion living in
|
|
191
|
+
# Config#root=.
|
|
192
|
+
def self.install_config_accessors
|
|
193
|
+
CONFIG_MAPPING.each do |name, (mod, mattr)|
|
|
194
|
+
[mod, mod.singleton_class].each do |target|
|
|
195
|
+
target.define_method(mattr) do
|
|
196
|
+
Deprecation.notice
|
|
197
|
+
SnapDiff.config.public_send(name)
|
|
198
|
+
end
|
|
199
|
+
next if name == :root && target == mod
|
|
200
|
+
|
|
201
|
+
target.define_method(:"#{mattr}=") do |value|
|
|
202
|
+
Deprecation.notice
|
|
203
|
+
SnapDiff.config.public_send(:"#{name}=", value)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# `include Capybara::Screenshot::Diff` is the third way into the v1
|
|
210
|
+
# surface (it picks up the instance-level accessors installed above) and
|
|
211
|
+
# resolves no deprecated constant of its own, so it needs its own hook
|
|
212
|
+
# for the once-per-process notice.
|
|
213
|
+
def self.install_include_notice(mod)
|
|
214
|
+
mod.define_singleton_method(:included) do |base|
|
|
215
|
+
Deprecation.notice
|
|
216
|
+
super(base)
|
|
50
217
|
end
|
|
51
218
|
end
|
|
52
219
|
end
|
|
53
220
|
end
|
|
54
221
|
|
|
222
|
+
SnapDiff::LegacyShims.install_config_accessors
|
|
223
|
+
SnapDiff::LegacyShims.install_include_notice(Capybara::Screenshot)
|
|
224
|
+
SnapDiff::LegacyShims.install_include_notice(Capybara::Screenshot::Diff)
|
|
225
|
+
|
|
226
|
+
module SnapDiff
|
|
227
|
+
# v1-style configuration: yields the two legacy accessor holders
|
|
228
|
+
# (+Capybara::Screenshot+, +Capybara::Screenshot::Diff+) exactly as
|
|
229
|
+
# +Capybara::Screenshot::Diff.configure+ always has -- and, since ADR-008
|
|
230
|
+
# step 7b, this is where that yield actually happens; Diff.configure
|
|
231
|
+
# forwards here. Both names stay identical in call shape.
|
|
232
|
+
#
|
|
233
|
+
# SnapDiff.start do |screenshot, diff|
|
|
234
|
+
# screenshot.window_size = [1280, 1024]
|
|
235
|
+
# diff.tolerance = 0.0005
|
|
236
|
+
# end
|
|
237
|
+
#
|
|
238
|
+
# Defined in this file, not snap_diff.rb, because the two holders it
|
|
239
|
+
# yields ARE the v1 surface: it cannot outlive them. The consolidated
|
|
240
|
+
# shape, SnapDiff.configure, is the canonical one and lives in the core.
|
|
241
|
+
def self.start
|
|
242
|
+
yield Capybara::Screenshot, Capybara::Screenshot::Diff
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
55
246
|
module Capybara
|
|
56
247
|
module Screenshot
|
|
248
|
+
# EAGER same-object alias (see header): the only place it can be eager
|
|
249
|
+
# for a canonical-only require, which is what a half-migrated app has.
|
|
250
|
+
Os = SnapDiff::Os
|
|
251
|
+
|
|
252
|
+
# Derived config, ex config_legacy.rb: one-line forwarders onto the
|
|
253
|
+
# canonical implementations in SnapDiff::Config (ADR-008 step 7b).
|
|
254
|
+
class << self
|
|
255
|
+
def active?
|
|
256
|
+
SnapDiff.config.active?
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def screenshot_area
|
|
260
|
+
SnapDiff.config.screenshot_area
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def screenshot_area_abs
|
|
264
|
+
SnapDiff.config.screenshot_area_abs
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
57
268
|
module Diff
|
|
58
|
-
# EAGER same-object
|
|
59
|
-
|
|
269
|
+
# EAGER same-object aliases of canonical values (see header for why
|
|
270
|
+
# each one is eager rather than a warn-once const_missing shim).
|
|
271
|
+
# .registry, not .loaded: this alias is assigned at load time by the
|
|
272
|
+
# gem itself, and .loaded announces its own 2.1 removal. Same object
|
|
273
|
+
# either way -- which is the whole point of the alias.
|
|
274
|
+
LOADED_DRIVERS = SnapDiff::Drivers.registry
|
|
275
|
+
AVAILABLE_DRIVERS = SnapDiff::Drivers::AVAILABLE_DRIVERS
|
|
276
|
+
Comparison = SnapDiff::Comparison::Images
|
|
277
|
+
VERSION = SnapDiff::VERSION
|
|
60
278
|
|
|
61
279
|
module Reporters
|
|
280
|
+
Default = SnapDiff::Reporters::Default
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
# Configure screenshot and diff settings in one block.
|
|
284
|
+
#
|
|
285
|
+
# Capybara::Screenshot::Diff.configure do |screenshot, diff|
|
|
286
|
+
# screenshot.window_size = [1280, 1024]
|
|
287
|
+
# screenshot.stability_time_limit = 1
|
|
288
|
+
# diff.driver = :vips
|
|
289
|
+
# diff.tolerance = 0.0005
|
|
290
|
+
# end
|
|
291
|
+
# The bare `yield` (rather than an explicit &block) keeps this
|
|
292
|
+
# method's published arity byte-identical to what it always had.
|
|
293
|
+
def self.configure
|
|
294
|
+
SnapDiff.start { |screenshot, diff| yield screenshot, diff }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def self.compare(baseline_path, current_path, **options)
|
|
298
|
+
SnapDiff.compare(baseline_path, current_path, **options)
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def self.default_options
|
|
302
|
+
SnapDiff::Deprecation.notice
|
|
303
|
+
SnapDiff.config.default_options
|
|
62
304
|
end
|
|
63
305
|
end
|
|
64
306
|
end
|
|
@@ -67,6 +309,19 @@ end
|
|
|
67
309
|
module CapybaraScreenshotDiff
|
|
68
310
|
module Reporters
|
|
69
311
|
end
|
|
312
|
+
|
|
313
|
+
# Predefined so the mapping below has a namespace to hang const_missing
|
|
314
|
+
# on; capybara_screenshot_diff/minitest reopens it with the eager alias.
|
|
315
|
+
module Minitest
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# EAGER same-object aliases (see header): rescue-by-old-name and
|
|
319
|
+
# defined?/const_defined? feature detection must behave as they always
|
|
320
|
+
# have, under canonical and legacy requires alike.
|
|
321
|
+
CapybaraScreenshotDiffError = SnapDiff::Error
|
|
322
|
+
ExpectationNotMet = SnapDiff::ExpectationNotMet
|
|
323
|
+
UnstableImage = SnapDiff::UnstableImage
|
|
324
|
+
WindowSizeMismatchError = SnapDiff::WindowSizeMismatchError
|
|
70
325
|
end
|
|
71
326
|
|
|
72
327
|
SnapDiff::LegacyShims.install(Capybara::Screenshot, "Capybara::Screenshot", {
|
|
@@ -84,14 +339,9 @@ SnapDiff::LegacyShims.install(Capybara::Screenshot::Diff, "Capybara::Screenshot:
|
|
|
84
339
|
ScreenshotMatcher: "SnapDiff::ScreenshotMatcher",
|
|
85
340
|
Drivers: "SnapDiff::Drivers",
|
|
86
341
|
ImageCompare: "SnapDiff::Comparison",
|
|
87
|
-
Comparison: "SnapDiff::Comparison::Images",
|
|
88
342
|
Difference: "SnapDiff::ComparisonResult"
|
|
89
343
|
}.freeze)
|
|
90
344
|
|
|
91
|
-
SnapDiff::LegacyShims.install(Capybara::Screenshot::Diff::Reporters, "Capybara::Screenshot::Diff::Reporters", {
|
|
92
|
-
Default: "SnapDiff::Reporters::Default"
|
|
93
|
-
}.freeze)
|
|
94
|
-
|
|
95
345
|
SnapDiff::LegacyShims.install(CapybaraScreenshotDiff, "CapybaraScreenshotDiff", {
|
|
96
346
|
RED_RGBA: "SnapDiff::RED_RGBA",
|
|
97
347
|
ORANGE_RGBA: "SnapDiff::ORANGE_RGBA",
|
|
@@ -102,16 +352,21 @@ SnapDiff::LegacyShims.install(CapybaraScreenshotDiff, "CapybaraScreenshotDiff",
|
|
|
102
352
|
BacktraceFilter: "SnapDiff::BacktraceFilter",
|
|
103
353
|
ErrorWithFilteredBacktrace: "SnapDiff::ErrorWithFilteredBacktrace",
|
|
104
354
|
ScreenshotAssertion: "SnapDiff::ScreenshotAssertion",
|
|
105
|
-
AssertionRegistry: "SnapDiff::AssertionRegistry"
|
|
355
|
+
AssertionRegistry: "SnapDiff::AssertionRegistry",
|
|
356
|
+
DSL: "SnapDiff::DSL"
|
|
106
357
|
}.freeze)
|
|
107
358
|
|
|
108
359
|
SnapDiff::LegacyShims.install(CapybaraScreenshotDiff::Reporters, "CapybaraScreenshotDiff::Reporters", {
|
|
109
360
|
HTML: "SnapDiff::Reporters::HTML"
|
|
110
361
|
}.freeze)
|
|
111
362
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
363
|
+
SnapDiff::LegacyShims.install(CapybaraScreenshotDiff::Minitest, "CapybaraScreenshotDiff::Minitest", {
|
|
364
|
+
Assertions: "SnapDiff::Minitest::Assertions"
|
|
365
|
+
}.freeze)
|
|
366
|
+
|
|
367
|
+
# BaseDriver dissolved into the SnapDiff::Driver mixin; the Drivers alias is
|
|
368
|
+
# same-object, so the hook has to live on SnapDiff::Drivers itself.
|
|
369
|
+
# `class MyDriver < BaseDriver` becomes `include SnapDiff::Driver`.
|
|
115
370
|
SnapDiff::LegacyShims.install(SnapDiff::Drivers, "Capybara::Screenshot::Diff::Drivers", {
|
|
116
371
|
BaseDriver: "SnapDiff::Driver"
|
|
117
372
|
}.freeze)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SnapDiff
|
|
4
|
+
# @api private
|
|
5
|
+
#
|
|
6
|
+
# Announces what 2.1 REMOVES, from the 2.0 line that still supports it:
|
|
7
|
+
# the chunky_png driver, +shift_distance_limit+ (chunky-only, it dies with
|
|
8
|
+
# it) and the driver abstraction (+SnapDiff::Driver+,
|
|
9
|
+
# +SnapDiff::Drivers.loaded+ / +.available+, <tt>driver: :auto</tt>) --
|
|
10
|
+
# libvips becomes the only backend. 2.0 is the transitional release: the
|
|
11
|
+
# contract is published before it is enforced.
|
|
12
|
+
#
|
|
13
|
+
# Same shape and same silencing switches as {SnapDiff::Deprecation}, which
|
|
14
|
+
# announces the other half (the v1 namespaces), but a file of its own: the
|
|
15
|
+
# legacy shims and their deprecation channel are themselves part of what is
|
|
16
|
+
# removed, while the call sites here -- utils, config, drivers -- are core
|
|
17
|
+
# files that outlive them, so they cannot depend on a doomed file. That is
|
|
18
|
+
# also why {SnapDiff.silence_deprecations} lives HERE rather than in
|
|
19
|
+
# deprecation.rb: it is the one switch that silences both halves.
|
|
20
|
+
#
|
|
21
|
+
# Deliberately not a warn-per-call channel: one line per subject per
|
|
22
|
+
# process is an actionable signal, N lines per comparison is noise people
|
|
23
|
+
# learn to filter out.
|
|
24
|
+
module Removal
|
|
25
|
+
# Everything under lib/ is "the gem"; the first caller frame outside it
|
|
26
|
+
# is the user code that touched the doomed API.
|
|
27
|
+
GEM_LIB_DIR = File.expand_path("..", __dir__) + File::SEPARATOR
|
|
28
|
+
|
|
29
|
+
# Appended to every message, so the individual messages can stay about
|
|
30
|
+
# the thing being removed.
|
|
31
|
+
SILENCE_HINT =
|
|
32
|
+
"Silence with `SnapDiff.silence_deprecations = true` or " \
|
|
33
|
+
"SNAP_DIFF_SILENCE_DEPRECATIONS=1. (shown once per process)"
|
|
34
|
+
|
|
35
|
+
# The one message with two call sites -- the setting's writer (Config)
|
|
36
|
+
# and the per-comparison option (Comparison) -- so it lives here rather
|
|
37
|
+
# than in either of them. One subject, one warning, whichever fires.
|
|
38
|
+
SHIFT_DISTANCE_LIMIT_REMOVED =
|
|
39
|
+
"`shift_distance_limit` is REMOVED in 2.1: it is implemented only by the chunky_png " \
|
|
40
|
+
"driver, which is removed with it. libvips has no shift-distance comparison -- drop the " \
|
|
41
|
+
"option and tune `tolerance` / `color_distance_limit` instead. See docs/configuration.md."
|
|
42
|
+
|
|
43
|
+
# Also one subject with several call sites -- the setting's writer
|
|
44
|
+
# (Config) and the raw per-screenshot / per-compare option hashes.
|
|
45
|
+
# Deliberately value-blind: `driver: :vips` warns too. It is the knob
|
|
46
|
+
# that picks between implementations, and 2.1 leaves one implementation,
|
|
47
|
+
# so the line goes whatever it currently says.
|
|
48
|
+
DRIVER_REMOVED =
|
|
49
|
+
"The `driver` setting and the per-screenshot `driver:` option are REMOVED in 2.1: libvips " \
|
|
50
|
+
"becomes the only backend, so there is nothing left to select. Drop the option and depend " \
|
|
51
|
+
"on the `ruby-vips` gem instead. See docs/drivers.md."
|
|
52
|
+
|
|
53
|
+
# --- the new-screenshot booleans, superseded by `record:` (#259) -----
|
|
54
|
+
#
|
|
55
|
+
# Three separate settings, each answering some part of "what happens
|
|
56
|
+
# when there is no baseline / when there is a difference", none of them
|
|
57
|
+
# naming the action the user actually wants. 2.0 adds the verb --
|
|
58
|
+
# `SnapDiff.config.record` -- and 2.1 removes the booleans. Each message
|
|
59
|
+
# names the mode that replaces THAT setting, and nothing more: `record:`
|
|
60
|
+
# supersedes the missing-baseline half squarely and the other two only
|
|
61
|
+
# obliquely, and a message that overclaims is a message that misroutes.
|
|
62
|
+
|
|
63
|
+
FAIL_IF_NEW_REMOVED =
|
|
64
|
+
"`fail_if_new` is REMOVED in 2.1: the record modes replace it. `SnapDiff.config.record = :none` " \
|
|
65
|
+
"is `fail_if_new = true`, `= :once` is `fail_if_new = false` -- and unlike the boolean, a mode " \
|
|
66
|
+
"means the same thing on CI and off it. See docs/configuration.md."
|
|
67
|
+
|
|
68
|
+
PENDING_IF_NEW_REMOVED =
|
|
69
|
+
"`pending_if_new` is REMOVED in 2.1: it skips the test instead of saying what to do about the " \
|
|
70
|
+
"missing baseline. `SnapDiff.config.record = :none` fails with the `git add` command attached; " \
|
|
71
|
+
"`= :once` records it and reports it in the end-of-run summary. See docs/configuration.md."
|
|
72
|
+
|
|
73
|
+
FAIL_ON_DIFFERENCE_REMOVED =
|
|
74
|
+
"`fail_on_difference` is REMOVED in 2.1: a screenshot that differs from its baseline fails, " \
|
|
75
|
+
"which is what the gem is for. To ACCEPT a difference, re-record it -- " \
|
|
76
|
+
"`SnapDiff.config.record = :all` -- and commit the result. See docs/configuration.md."
|
|
77
|
+
|
|
78
|
+
# Not a removal: 2.0 announces it and 2.1 turns it into an ArgumentError.
|
|
79
|
+
# The whole reason unrecognised keys need announcing is that the options
|
|
80
|
+
# hash was frozen but never validated, so a typo -- or a v1 option that
|
|
81
|
+
# no longer exists -- configured nothing, silently, forever.
|
|
82
|
+
def self.unknown_option(key)
|
|
83
|
+
"`#{key.inspect}` is not a recognised screenshot option, so it does nothing. 2.1 raises " \
|
|
84
|
+
"ArgumentError for it. Check the spelling against the option list in docs/configuration.md."
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
MUTEX = Mutex.new
|
|
88
|
+
@seen = {}
|
|
89
|
+
@suppressed = false
|
|
90
|
+
|
|
91
|
+
class << self
|
|
92
|
+
# Emit +message+ once per +subject+ per process, via Kernel#warn (so
|
|
93
|
+
# anything hooking +Warning.warn+ sees it like any other Ruby warning).
|
|
94
|
+
#
|
|
95
|
+
# @param subject [Symbol] dedup key -- the doomed API, not the call site
|
|
96
|
+
# @param message [String] what is removed, when, and what to do instead
|
|
97
|
+
# @return [void]
|
|
98
|
+
def warn_once(subject, message)
|
|
99
|
+
return if @suppressed || SnapDiff.silence_deprecations?
|
|
100
|
+
|
|
101
|
+
first_time = MUTEX.synchronize { @seen.key?(subject) ? false : (@seen[subject] = true) }
|
|
102
|
+
return unless first_time
|
|
103
|
+
|
|
104
|
+
Kernel.warn(with_origin("[snap_diff deprecation] #{message} #{SILENCE_HINT}", caller_locations(1)))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# @api private
|
|
108
|
+
#
|
|
109
|
+
# Silences these warnings for the rest of the process, without touching
|
|
110
|
+
# the v1-namespace ones. For hosts that exercise the doomed APIs BY
|
|
111
|
+
# DESIGN rather than depending on them -- this gem's own suite runs the
|
|
112
|
+
# whole comparison matrix on chunky_png and sets shift_distance_limit,
|
|
113
|
+
# and its test_helper raises on any deprecation output.
|
|
114
|
+
# @return [void]
|
|
115
|
+
def suppress!
|
|
116
|
+
MUTEX.synchronize { @suppressed = true }
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private
|
|
120
|
+
|
|
121
|
+
def with_origin(message, locations)
|
|
122
|
+
origin = origin_for(locations)
|
|
123
|
+
origin ? "#{message} (called from #{origin})" : message
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# First frame outside the gem's lib dir, formatted "file:line"; nil
|
|
127
|
+
# when every frame is internal (or paths are unavailable).
|
|
128
|
+
def origin_for(locations)
|
|
129
|
+
(locations || []).each do |location|
|
|
130
|
+
path = location.absolute_path || location.path
|
|
131
|
+
next if path.nil? || path.start_with?(GEM_LIB_DIR)
|
|
132
|
+
|
|
133
|
+
return "#{path}:#{location.lineno}"
|
|
134
|
+
end
|
|
135
|
+
nil
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class << self
|
|
141
|
+
# @api private
|
|
142
|
+
attr_accessor :silence_deprecations
|
|
143
|
+
|
|
144
|
+
# @api private
|
|
145
|
+
#
|
|
146
|
+
# @return [Boolean] true if deprecation warnings should be suppressed,
|
|
147
|
+
# either via the {silence_deprecations} accessor or the
|
|
148
|
+
# SNAP_DIFF_SILENCE_DEPRECATIONS env var (truthy = "1"/"true").
|
|
149
|
+
def silence_deprecations?
|
|
150
|
+
!!silence_deprecations || truthy_env?(ENV["SNAP_DIFF_SILENCE_DEPRECATIONS"])
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private
|
|
154
|
+
|
|
155
|
+
def truthy_env?(value)
|
|
156
|
+
%w[1 true].include?(value.to_s.downcase)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
3
5
|
require "snap_diff/annotation_service"
|
|
6
|
+
# For SnapDiff.config.root, the base for the relative artifact paths below.
|
|
7
|
+
require "snap_diff/config"
|
|
4
8
|
|
|
5
9
|
module SnapDiff
|
|
6
10
|
module Reporters
|
|
@@ -47,22 +51,6 @@ module SnapDiff
|
|
|
47
51
|
annotation_service.annotate_and_save_images
|
|
48
52
|
end
|
|
49
53
|
|
|
50
|
-
def save_annotation_for(image, image_path)
|
|
51
|
-
annotation_service.save_annotation_for(image, image_path)
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def annotate_difference(image, region)
|
|
55
|
-
annotation_service.annotate_difference(image, region)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def annotate_skip_areas(image, skip_areas)
|
|
59
|
-
annotation_service.annotate_skip_areas(image, skip_areas)
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def save(image, image_path)
|
|
63
|
-
annotation_service.save(image, image_path)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
54
|
def build_error_for_different_dimensions
|
|
67
55
|
change_msg = [comparison.base_image, comparison.new_image]
|
|
68
56
|
.map { |image| driver.dimension(image).join("x") }
|
|
@@ -73,20 +61,95 @@ module SnapDiff
|
|
|
73
61
|
|
|
74
62
|
NEW_LINE = "\n"
|
|
75
63
|
|
|
64
|
+
# The thresholds a comparison is judged against, in the order they read
|
|
65
|
+
# best. Only the ones actually set are printed -- see #thresholds.
|
|
66
|
+
THRESHOLDS = [
|
|
67
|
+
:tolerance,
|
|
68
|
+
:area_size_limit,
|
|
69
|
+
:color_distance_limit,
|
|
70
|
+
:shift_distance_limit,
|
|
71
|
+
:perceptual_threshold
|
|
72
|
+
].freeze
|
|
73
|
+
|
|
74
|
+
# The five artifacts a comparison can leave on disk, in the order a
|
|
75
|
+
# reader wants them: the two inputs first, then what we drew on them.
|
|
76
|
+
ARTIFACT_LABELS = {
|
|
77
|
+
"baseline" => :base_image_path,
|
|
78
|
+
"actual" => :image_path,
|
|
79
|
+
"baseline annotated" => :annotated_base_image_path,
|
|
80
|
+
"actual annotated" => :annotated_image_path,
|
|
81
|
+
"heatmap" => :heatmap_diff_path
|
|
82
|
+
}.freeze
|
|
83
|
+
|
|
76
84
|
def build_error_message
|
|
77
|
-
[
|
|
78
|
-
"(#{difference.to_h.to_json})",
|
|
79
|
-
image_path.to_path,
|
|
80
|
-
annotated_base_image_path.to_path,
|
|
81
|
-
annotated_image_path.to_path,
|
|
82
|
-
heatmap_diff_path.to_path
|
|
83
|
-
].join(NEW_LINE)
|
|
85
|
+
[headline, *metric_lines, *artifact_lines].join(NEW_LINE)
|
|
84
86
|
end
|
|
85
87
|
|
|
86
88
|
private
|
|
87
89
|
|
|
88
90
|
attr_reader :annotation_service
|
|
89
91
|
|
|
92
|
+
# Reads as the tail of "Screenshot does not match for 'name': ".
|
|
93
|
+
def headline
|
|
94
|
+
width, height = driver.dimension(comparison.base_image)
|
|
95
|
+
total_pixels = width * height
|
|
96
|
+
area = difference.region_area_size
|
|
97
|
+
|
|
98
|
+
"the change spans #{area.round} of #{total_pixels} px " \
|
|
99
|
+
"(#{percent(area.to_f / total_pixels)} of the #{width}x#{height} image)"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def metric_lines
|
|
103
|
+
lines = [" changed region: #{difference.coordinates.to_json} (left,top,right,bottom edges)"]
|
|
104
|
+
# difference_level is the changed share of the image area -- the
|
|
105
|
+
# number `tolerance` is compared against. Only computed when a
|
|
106
|
+
# tolerance is set, so only printed then.
|
|
107
|
+
if difference.ratio
|
|
108
|
+
lines << " difference level: #{difference.ratio} (#{percent(difference.ratio)} of the image area)"
|
|
109
|
+
end
|
|
110
|
+
max_color_distance = difference.meta[:max_color_distance]
|
|
111
|
+
lines << " max color distance: #{max_color_distance}" if max_color_distance&.positive?
|
|
112
|
+
max_shift_distance = difference.meta[:max_shift_distance]
|
|
113
|
+
lines << " max shift distance: #{max_shift_distance} px" if max_shift_distance&.positive?
|
|
114
|
+
lines << " judged against: #{thresholds}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def thresholds
|
|
118
|
+
applied = THRESHOLDS.filter_map do |name|
|
|
119
|
+
value = difference.options[name]
|
|
120
|
+
"#{name} #{value}" if value
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
applied.empty? ? "no tolerance thresholds configured (any difference fails)" : applied.join(", ")
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Only what is on disk gets a line: the heatmap exists solely for
|
|
127
|
+
# drivers that produce a diff mask, and a comparison can be reported
|
|
128
|
+
# before either input has been written out.
|
|
129
|
+
def artifact_lines
|
|
130
|
+
present = ARTIFACT_LABELS.filter_map do |label, path_method|
|
|
131
|
+
path = send(path_method)
|
|
132
|
+
[label, path] if path.exist?
|
|
133
|
+
end
|
|
134
|
+
width = present.map { |label, _path| label.length }.max.to_i
|
|
135
|
+
|
|
136
|
+
present.map { |label, path| " #{"#{label}:".ljust(width + 1)} #{display_path(path)}" }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Relative to the configured root: shorter to read, and still
|
|
140
|
+
# click-through-able in terminals that resolve paths against the
|
|
141
|
+
# working directory. Anything outside the root stays absolute, because
|
|
142
|
+
# a "../../.." path is neither.
|
|
143
|
+
def display_path(path)
|
|
144
|
+
relative = path.expand_path.relative_path_from(SnapDiff.config.root).to_path
|
|
145
|
+
relative.start_with?("..") ? path.to_path : relative
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def percent(fraction)
|
|
149
|
+
value = fraction * 100
|
|
150
|
+
(value.positive? && value < 0.01) ? "<0.01%" : format("%.2f%%", value)
|
|
151
|
+
end
|
|
152
|
+
|
|
90
153
|
def base_image_path
|
|
91
154
|
comparison.base_image_path
|
|
92
155
|
end
|