snap_diff-capybara 2.0.0.alpha1 → 2.0.0.beta2
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 +79 -0
- data/docs/UPGRADING.md +243 -1
- data/docs/architecture.md +91 -53
- data/docs/ci-integration.md +13 -3
- data/docs/configuration.md +31 -2
- data/docs/drivers.md +6 -0
- data/docs/framework-setup.md +21 -3
- data/docs/reporters.md +20 -5
- data/docs/snapdiff.md +326 -0
- data/docs/thread_safety.md +4 -3
- data/lib/capybara/screenshot/diff/config_legacy.rb +26 -72
- data/lib/capybara/screenshot/diff/image_compare.rb +5 -2
- data/lib/capybara/screenshot/diff/region.rb +3 -105
- data/lib/capybara/screenshot/diff/reporters/default.rb +4 -106
- data/lib/capybara_screenshot_diff/screenshot_assertion.rb +12 -24
- data/lib/capybara_screenshot_diff.rb +10 -4
- data/lib/snap_diff/area_calculator.rb +1 -3
- data/lib/snap_diff/browser_helpers.rb +1 -3
- data/lib/snap_diff/capture/viewport.rb +6 -7
- data/lib/snap_diff/comparison.rb +15 -28
- data/lib/snap_diff/config.rb +151 -29
- data/lib/snap_diff/deprecation.rb +26 -8
- data/lib/snap_diff/drivers.rb +20 -0
- data/lib/snap_diff/dsl.rb +12 -12
- data/lib/snap_diff/errors.rb +19 -0
- data/lib/snap_diff/integrations/cucumber.rb +5 -4
- data/lib/snap_diff/integrations/minitest.rb +11 -12
- data/lib/snap_diff/integrations/rspec.rb +7 -6
- data/lib/snap_diff/legacy_shims.rb +18 -0
- data/lib/snap_diff/region.rb +117 -0
- data/lib/snap_diff/reporters/default.rb +107 -0
- data/lib/snap_diff/reporters/html.rb +7 -9
- data/lib/snap_diff/reporting.rb +13 -4
- data/lib/snap_diff/screenshot_assertion.rb +37 -4
- data/lib/snap_diff/screenshot_matcher.rb +4 -4
- data/lib/snap_diff/screenshoter.rb +1 -1
- data/lib/snap_diff/snap_manager.rb +1 -2
- data/lib/snap_diff/stable_screenshoter.rb +2 -2
- data/lib/snap_diff/utils.rb +5 -3
- data/lib/snap_diff/version.rb +1 -1
- data/lib/snap_diff.rb +44 -15
- metadata +5 -1
data/lib/snap_diff/config.rb
CHANGED
|
@@ -1,35 +1,50 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
# This file is the LEAF of the config require graph (ADR-008 step 1):
|
|
6
|
+
# config_legacy.rb requires it, so it must never require config_legacy nor
|
|
7
|
+
# anything that leads back to either entry point. The MAPPING below needs
|
|
8
|
+
# the legacy module constants to exist at class-body eval time, so the empty
|
|
9
|
+
# skeleton is predefined here (same technique as legacy_shims.rb);
|
|
10
|
+
# config_legacy.rb reopens these modules and installs the delegating
|
|
11
|
+
# accessors from MAPPING.
|
|
12
|
+
module Capybara
|
|
13
|
+
module Screenshot
|
|
14
|
+
module Diff
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Referenced by Config#initialize (screenshoter/manager defaults), which
|
|
20
|
+
# runs at the eager Config.new at the bottom of this file, so they must be
|
|
21
|
+
# real, already-loaded classes first. Neither requires back here.
|
|
22
|
+
require "snap_diff/screenshoter"
|
|
23
|
+
require "snap_diff/snap_manager"
|
|
9
24
|
|
|
10
25
|
module SnapDiff
|
|
11
|
-
# Flat
|
|
12
|
-
# +Capybara::Screenshot
|
|
13
|
-
#
|
|
26
|
+
# Flat consolidation of every legacy +Capybara::Screenshot+ /
|
|
27
|
+
# +Capybara::Screenshot::Diff+ setting behind one object:
|
|
28
|
+
# <tt>SnapDiff.config.<em>attr</em></tt>.
|
|
29
|
+
#
|
|
30
|
+
# Storage ownership (ADR-008 step 1, inverted from the original v2
|
|
31
|
+
# consolidation): Config IS the single storage. The legacy accessors on
|
|
32
|
+
# +Capybara::Screenshot+ / +Capybara::Screenshot::Diff+ are thin
|
|
33
|
+
# delegators installed by config_legacy.rb from {MAPPING} -- one storage,
|
|
34
|
+
# two views, so a write through either surface is visible through the
|
|
35
|
+
# other structurally, not by synchronization.
|
|
14
36
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
# +
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
# is exactly the kind of "when defaults are evaluated" divergence the v2
|
|
25
|
-
# consolidation must not introduce. Delegating sidesteps the question
|
|
26
|
-
# entirely: Config never evaluates a default, it only ever forwards to
|
|
27
|
-
# whichever accessor already owns one. Bidirectional consistency (a write
|
|
28
|
-
# through either the old accessor or Config is visible through the other)
|
|
29
|
-
# falls out for free, because both paths read and write the exact same
|
|
30
|
-
# class-variable-backed storage.
|
|
37
|
+
# Default timing contract (pinned by config_default_timing_test.rb):
|
|
38
|
+
# every default below is evaluated ONCE, in #initialize, which runs at
|
|
39
|
+
# require time of this file (the eager +Config.new+ at the bottom) -- the
|
|
40
|
+
# same load moment the old +mattr_accessor+ default blocks evaluated at.
|
|
41
|
+
# In particular +fail_if_new+ (from <tt>ENV["CI"]</tt>) and +root+ (from
|
|
42
|
+
# +Rails.root+ / pwd) must never become lazy read-time defaults, memoized
|
|
43
|
+
# or not. The one deliberately LIVE value, +default_options[:wait]+, is
|
|
44
|
+
# not storage at all: it stays a method-body read of
|
|
45
|
+
# +Capybara.default_max_wait_time+ in +#default_options+.
|
|
31
46
|
class Config
|
|
32
|
-
# config attr name => [
|
|
47
|
+
# config attr name => [legacy module, legacy accessor name].
|
|
33
48
|
#
|
|
34
49
|
# The two names differ only for +screenshot_enabled+:
|
|
35
50
|
# +Capybara::Screenshot.enabled+ and +Capybara::Screenshot::Diff.enabled+
|
|
@@ -70,9 +85,116 @@ module SnapDiff
|
|
|
70
85
|
manager: [Capybara::Screenshot::Diff, :manager]
|
|
71
86
|
}.freeze
|
|
72
87
|
|
|
73
|
-
MAPPING.
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
attr_accessor(*(MAPPING.keys - [:root]))
|
|
89
|
+
attr_reader :root
|
|
90
|
+
|
|
91
|
+
def initialize
|
|
92
|
+
# Every mapped setting gets its ivar up front (nil-defaulted ones
|
|
93
|
+
# included) so the full set always exists -- test_helper's per-test
|
|
94
|
+
# isolation snapshots/restores config by instance variable, and an
|
|
95
|
+
# ivar that only appears on first write would escape that snapshot
|
|
96
|
+
# and leak between tests.
|
|
97
|
+
MAPPING.each_key { |key| instance_variable_set(:"@#{key}", nil) }
|
|
98
|
+
# Capybara::Screenshot side.
|
|
99
|
+
@blur_active_element = true
|
|
100
|
+
@hide_caret = true
|
|
101
|
+
# Raw Rails.root (no coercion), matching the old mattr_reader default;
|
|
102
|
+
# only the writer below coerces.
|
|
103
|
+
@root = (defined?(Rails) && defined?(Rails.root) && Rails.root) || Pathname(".").expand_path
|
|
104
|
+
@save_path = "doc/screenshots"
|
|
105
|
+
@screenshot_format = "png"
|
|
106
|
+
@capybara_screenshot_options = {}
|
|
107
|
+
# Capybara::Screenshot::Diff side.
|
|
108
|
+
@delayed = true
|
|
109
|
+
@fail_if_new = !ENV["CI"].nil? && !ENV["CI"].empty?
|
|
110
|
+
@pending_if_new = false
|
|
111
|
+
@fail_on_difference = true
|
|
112
|
+
@enabled = true
|
|
113
|
+
@driver = :auto
|
|
114
|
+
@screenshoter = SnapDiff::Screenshoter
|
|
115
|
+
@manager = SnapDiff::SnapManager
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def root=(path)
|
|
119
|
+
@root = Pathname(path).expand_path
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# --- Derived config (ADR-008 step 7b) -------------------------------
|
|
123
|
+
# Read-only values computed from the storage above. They used to live
|
|
124
|
+
# on the legacy modules; those now one-line forward here.
|
|
125
|
+
|
|
126
|
+
# ex +Capybara::Screenshot.active?+. The two +enabled+ settings are
|
|
127
|
+
# independent (see {MAPPING}): the Screenshot-side one wins whenever it
|
|
128
|
+
# was set at all, and only a nil there falls through to the Diff-side
|
|
129
|
+
# one.
|
|
130
|
+
def active?
|
|
131
|
+
screenshot_enabled || (screenshot_enabled.nil? && enabled)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# ex +Capybara::Screenshot.screenshot_area+: the save_path, optionally
|
|
135
|
+
# segmented per OS and per Capybara driver.
|
|
136
|
+
def screenshot_area
|
|
137
|
+
parts = [save_path]
|
|
138
|
+
parts << Os.name if add_os_path
|
|
139
|
+
parts << Capybara.current_driver.to_s if add_driver_path
|
|
140
|
+
File.join(*parts)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# ex +Capybara::Screenshot.screenshot_area_abs+.
|
|
144
|
+
def screenshot_area_abs
|
|
145
|
+
root / screenshot_area
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
# ex +Capybara::Screenshot::Diff.default_options+: the capture/compare
|
|
149
|
+
# defaults handed to {SnapDiff::Comparison}. Carries the one literal
|
|
150
|
+
# that is not a stored setting -- the vips tolerance floor.
|
|
151
|
+
def default_options
|
|
152
|
+
{
|
|
153
|
+
area_size_limit: area_size_limit,
|
|
154
|
+
color_distance_limit: color_distance_limit,
|
|
155
|
+
driver: driver,
|
|
156
|
+
screenshot_format: screenshot_format,
|
|
157
|
+
capybara_screenshot_options: capybara_screenshot_options,
|
|
158
|
+
perceptual_threshold: perceptual_threshold,
|
|
159
|
+
shift_distance_limit: shift_distance_limit,
|
|
160
|
+
skip_area: skip_area,
|
|
161
|
+
stability_time_limit: stability_time_limit,
|
|
162
|
+
tolerance: tolerance || ((driver == :vips) ? 0.001 : nil),
|
|
163
|
+
# Deliberately LIVE (pinned by config_default_timing_test.rb):
|
|
164
|
+
# read at call time, never frozen into storage.
|
|
165
|
+
wait: Capybara.default_max_wait_time
|
|
166
|
+
}
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Instantiated eagerly so the require-time defaults above are evaluated
|
|
171
|
+
# NOW, at load, not at the first SnapDiff.config call.
|
|
172
|
+
@config = Config.new
|
|
173
|
+
|
|
174
|
+
# The single consolidated settings object -- and the single storage.
|
|
175
|
+
# See {SnapDiff::Config}.
|
|
176
|
+
def self.config
|
|
177
|
+
@config
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Installs the old mattr_accessor surface onto the legacy modules,
|
|
181
|
+
# delegating to the single storage above. mattr_accessor used to define
|
|
182
|
+
# both singleton and instance accessors (the instance ones are what
|
|
183
|
+
# `include Capybara::Screenshot::Diff` picks up), so both are installed.
|
|
184
|
+
# root keeps its historical asymmetry -- readable everywhere, writable
|
|
185
|
+
# only at module level (it was mattr_reader plus a custom module-level
|
|
186
|
+
# writer) -- with the Pathname coercion living in Config#root=.
|
|
187
|
+
#
|
|
188
|
+
# Generated here rather than in config_legacy.rb (ADR-008 step 7b) for
|
|
189
|
+
# the same reason legacy_shims.rb generates the legacy constants here:
|
|
190
|
+
# the generator is code, and the v1 trees must stay alias-only so 3.0 is
|
|
191
|
+
# a `git rm`. Same technique, same side of the fence.
|
|
192
|
+
Config::MAPPING.each do |name, (mod, mattr)|
|
|
193
|
+
[mod, mod.singleton_class].each do |target|
|
|
194
|
+
target.define_method(mattr) { SnapDiff.config.public_send(name) }
|
|
195
|
+
next if name == :root && target == mod
|
|
196
|
+
|
|
197
|
+
target.define_method(:"#{mattr}=") { |value| SnapDiff.config.public_send(:"#{name}=", value) }
|
|
76
198
|
end
|
|
77
199
|
end
|
|
78
200
|
end
|
|
@@ -5,12 +5,16 @@ module SnapDiff
|
|
|
5
5
|
#
|
|
6
6
|
# Internal until the v2 namespace transition; not a public contract.
|
|
7
7
|
#
|
|
8
|
-
# Warn-once-per-subject deprecation
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
8
|
+
# Warn-once-per-subject deprecation engine for the legacy-namespace
|
|
9
|
+
# shims: snap_diff/legacy_shims routes every +const_missing+ hit on an
|
|
10
|
+
# old +Capybara::Screenshot::Diff+ / +CapybaraScreenshotDiff+ constant
|
|
11
|
+
# through {.warn}, so each deprecated name warns exactly once per
|
|
12
|
+
# process (ADR-004's v2 namespace transition).
|
|
13
13
|
module Deprecation
|
|
14
|
+
# Everything under lib/ is "the gem"; the first caller frame outside
|
|
15
|
+
# it is the user code that referenced the deprecated name (same
|
|
16
|
+
# filtering idea as BacktraceFilter in error_with_filtered_backtrace).
|
|
17
|
+
GEM_LIB_DIR = File.expand_path("..", __dir__) + File::SEPARATOR
|
|
14
18
|
# Emission channel: Kernel#warn, not a direct +$stderr.puts+.
|
|
15
19
|
#
|
|
16
20
|
# Kernel#warn delegates to +Warning.warn+ (Ruby >= 2.4), so anything
|
|
@@ -40,7 +44,7 @@ module SnapDiff
|
|
|
40
44
|
end
|
|
41
45
|
return unless first_time
|
|
42
46
|
|
|
43
|
-
Kernel.warn(message_for(subject, replacement, category))
|
|
47
|
+
Kernel.warn(message_for(subject, replacement, category, caller_locations(1)))
|
|
44
48
|
end
|
|
45
49
|
|
|
46
50
|
# @api private
|
|
@@ -55,9 +59,23 @@ module SnapDiff
|
|
|
55
59
|
|
|
56
60
|
private
|
|
57
61
|
|
|
58
|
-
def message_for(subject, replacement, category)
|
|
59
|
-
"[snap_diff deprecation] `#{subject}` is deprecated (#{category}); " \
|
|
62
|
+
def message_for(subject, replacement, category, locations)
|
|
63
|
+
message = "[snap_diff deprecation] `#{subject}` is deprecated (#{category}); " \
|
|
60
64
|
"use `#{replacement}` instead."
|
|
65
|
+
origin = origin_for(locations)
|
|
66
|
+
origin ? "#{message} (called from #{origin})" : message
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# First frame outside the gem's lib dir, formatted "file:line";
|
|
70
|
+
# nil when every frame is internal (or paths are unavailable).
|
|
71
|
+
def origin_for(locations)
|
|
72
|
+
(locations || []).each do |location|
|
|
73
|
+
path = location.absolute_path || location.path
|
|
74
|
+
next if path.nil? || path.start_with?(GEM_LIB_DIR)
|
|
75
|
+
|
|
76
|
+
return "#{path}:#{location.lineno}"
|
|
77
|
+
end
|
|
78
|
+
nil
|
|
61
79
|
end
|
|
62
80
|
end
|
|
63
81
|
end
|
data/lib/snap_diff/drivers.rb
CHANGED
|
@@ -10,5 +10,25 @@ module SnapDiff
|
|
|
10
10
|
|
|
11
11
|
Utils.find_driver_class_for(driver_option).new
|
|
12
12
|
end
|
|
13
|
+
|
|
14
|
+
# Canonical driver-class cache (ADR-008 step 5b, ex
|
|
15
|
+
# Capybara::Screenshot::Diff::LOADED_DRIVERS): driver name => driver
|
|
16
|
+
# class, filled lazily by Utils.find_driver_class_for. Mutated in
|
|
17
|
+
# place -- including by user registration through the legacy constant,
|
|
18
|
+
# which legacy_shims pins as an EAGER same-object alias of this hash
|
|
19
|
+
# (a lazy copy would silently drop such registrations).
|
|
20
|
+
def self.loaded
|
|
21
|
+
@loaded ||= {}
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Canonical read API for the detected-drivers list. The value itself
|
|
25
|
+
# stays on Capybara::Screenshot::Diff::AVAILABLE_DRIVERS (assigned in
|
|
26
|
+
# config_legacy.rb at load time, exactly when detection historically
|
|
27
|
+
# ran); this reads it live rather than caching, because that constant
|
|
28
|
+
# is the published stubbing point (image_compare_test stubs it to []
|
|
29
|
+
# to exercise the no-drivers error path).
|
|
30
|
+
def self.available
|
|
31
|
+
Capybara::Screenshot::Diff::AVAILABLE_DRIVERS
|
|
32
|
+
end
|
|
13
33
|
end
|
|
14
34
|
end
|
data/lib/snap_diff/dsl.rb
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# No require of "capybara_screenshot_diff" here: every Capybara::Screenshot
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
4
|
+
# reference below is inside a method body, resolved lazily at call time, not
|
|
5
|
+
# at this file's own load time -- so this unit has no eager dependency on
|
|
6
|
+
# the umbrella entry point, and requiring it here would create a require
|
|
7
|
+
# cycle back to capybara_screenshot_diff.rb (which requires this file's
|
|
8
|
+
# old-path forwarder).
|
|
9
9
|
# DSL includes Capybara::DSL directly below, so it needs the base gem
|
|
10
10
|
# loaded regardless of what pulled this file in.
|
|
11
11
|
require "capybara/dsl"
|
|
@@ -59,14 +59,14 @@ module SnapDiff
|
|
|
59
59
|
# @option options [Numeric] :area_size_limit Maximum allowed difference area size in pixels.
|
|
60
60
|
# @option options [Symbol] :driver (:auto) The image processing driver to use (:auto, :chunky_png, :vips).
|
|
61
61
|
# @return [Boolean] True if the screenshot was successfully captured and processed.
|
|
62
|
-
# @raise [
|
|
63
|
-
# @raise [
|
|
64
|
-
# @raise [
|
|
62
|
+
# @raise [SnapDiff::ExpectationNotMet] If comparison fails and immediate validation is enabled.
|
|
63
|
+
# @raise [SnapDiff::UnstableImage] If the image comparison is unstable.
|
|
64
|
+
# @raise [SnapDiff::WindowSizeMismatchError] If the window size doesn't match expectations.
|
|
65
65
|
def assert_matches_screenshot(name, skip_stack_frames: 0, **options)
|
|
66
66
|
return false unless Capybara::Screenshot.active?
|
|
67
67
|
|
|
68
68
|
# Get the full name with section and group information
|
|
69
|
-
full_name =
|
|
69
|
+
full_name = SnapDiff.session.screenshot_namer.full_name(name)
|
|
70
70
|
|
|
71
71
|
# Build the screenshot assertion
|
|
72
72
|
assertion = build_screenshot_assertion(full_name, options, skip_stack_frames: skip_stack_frames + 1)
|
|
@@ -77,7 +77,7 @@ module SnapDiff
|
|
|
77
77
|
delayed = options.fetch(:delayed, Capybara::Screenshot::Diff.delayed)
|
|
78
78
|
|
|
79
79
|
if delayed
|
|
80
|
-
|
|
80
|
+
SnapDiff.session.add_assertion(assertion)
|
|
81
81
|
else
|
|
82
82
|
assertion.validate!
|
|
83
83
|
end
|
|
@@ -104,7 +104,7 @@ module SnapDiff
|
|
|
104
104
|
def capture_screenshot(name, **options)
|
|
105
105
|
return false unless Capybara::Screenshot.active?
|
|
106
106
|
|
|
107
|
-
full_name =
|
|
107
|
+
full_name = SnapDiff.session.screenshot_namer.full_name(name)
|
|
108
108
|
SnapDiff::ScreenshotMatcher.new(full_name, options).capture
|
|
109
109
|
|
|
110
110
|
true
|
|
@@ -137,7 +137,7 @@ module SnapDiff
|
|
|
137
137
|
end
|
|
138
138
|
|
|
139
139
|
def screenshot_namer
|
|
140
|
-
|
|
140
|
+
SnapDiff.session.screenshot_namer
|
|
141
141
|
end
|
|
142
142
|
end
|
|
143
143
|
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "snap_diff/error_with_filtered_backtrace"
|
|
4
|
+
|
|
5
|
+
# ADR-008 step 2: the gem's error classes live under SnapDiff. The old
|
|
6
|
+
# CapybaraScreenshotDiff names (capybara_screenshot_diff.rb) are EAGER
|
|
7
|
+
# same-object aliases of these classes -- deliberately not const_missing
|
|
8
|
+
# shims, because rescue clauses and defined?/const_defined? feature
|
|
9
|
+
# detection in adopter code must keep behaving exactly as before
|
|
10
|
+
# (const_defined? never triggers const_missing).
|
|
11
|
+
module SnapDiff
|
|
12
|
+
class Error < ErrorWithFilteredBacktrace; end
|
|
13
|
+
|
|
14
|
+
class ExpectationNotMet < Error; end
|
|
15
|
+
|
|
16
|
+
class UnstableImage < Error; end
|
|
17
|
+
|
|
18
|
+
class WindowSizeMismatchError < ErrorWithFilteredBacktrace; end
|
|
19
|
+
end
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
# See the matching comment in integrations/minitest.rb.
|
|
4
4
|
require_relative "../dsl"
|
|
5
|
-
require "
|
|
5
|
+
require "snap_diff/screenshot_assertion"
|
|
6
|
+
require "snap_diff/reporting"
|
|
6
7
|
|
|
7
8
|
World(::SnapDiff::DSL)
|
|
8
9
|
|
|
@@ -12,11 +13,11 @@ Before do
|
|
|
12
13
|
end
|
|
13
14
|
|
|
14
15
|
After do |scenario|
|
|
15
|
-
if !scenario.failed? && (msg =
|
|
16
|
+
if !scenario.failed? && (msg = SnapDiff.pending_screenshots_message)
|
|
16
17
|
skip_this_scenario(msg)
|
|
17
18
|
end
|
|
18
19
|
ensure
|
|
19
|
-
|
|
20
|
+
SnapDiff.reset
|
|
20
21
|
end
|
|
21
22
|
|
|
22
|
-
AfterAll {
|
|
23
|
+
AfterAll { SnapDiff::Reporting.finalize! }
|
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
require "minitest"
|
|
4
4
|
require_relative "../dsl"
|
|
5
|
-
#
|
|
6
|
-
# .
|
|
7
|
-
#
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
require "capybara_screenshot_diff/screenshot_assertion"
|
|
5
|
+
# SnapDiff.session/.reset/.pending_screenshots_message live in
|
|
6
|
+
# snap_diff/screenshot_assertion, SnapDiff::Reporting.finalize! in
|
|
7
|
+
# snap_diff/reporting -- neither is pulled in by requiring dsl.rb alone.
|
|
8
|
+
require "snap_diff/screenshot_assertion"
|
|
9
|
+
require "snap_diff/reporting"
|
|
11
10
|
|
|
12
11
|
used_deprecated_entrypoint = caller.any? do |path|
|
|
13
12
|
path.include?("capybara-screenshot-diff.rb") || path.include?("capybara/screenshot/diff.rb")
|
|
@@ -29,7 +28,7 @@ module SnapDiff
|
|
|
29
28
|
self.assertions += 1
|
|
30
29
|
|
|
31
30
|
super(*args, skip_stack_frames: skip_stack_frames + 1, **opts)
|
|
32
|
-
rescue ::
|
|
31
|
+
rescue ::SnapDiff::ExpectationNotMet => e
|
|
33
32
|
raise ::Minitest::Assertion, e.message
|
|
34
33
|
end
|
|
35
34
|
|
|
@@ -40,18 +39,18 @@ module SnapDiff
|
|
|
40
39
|
|
|
41
40
|
def before_teardown
|
|
42
41
|
super
|
|
43
|
-
|
|
42
|
+
SnapDiff.session.verify
|
|
44
43
|
|
|
45
44
|
# Computed here (before teardown/reset), but the actual `skip` is
|
|
46
45
|
# deferred to `after_teardown` so a real error raised by the user's
|
|
47
46
|
# `teardown` isn't masked by a pending skip recorded before it ran.
|
|
48
|
-
@capybara_screenshot_diff_pending_message =
|
|
49
|
-
rescue
|
|
47
|
+
@capybara_screenshot_diff_pending_message = SnapDiff.pending_screenshots_message
|
|
48
|
+
rescue SnapDiff::ExpectationNotMet => e
|
|
50
49
|
assertion = ::Minitest::Assertion.new(e)
|
|
51
50
|
assertion.set_backtrace(e.backtrace)
|
|
52
51
|
failures << assertion
|
|
53
52
|
ensure
|
|
54
|
-
|
|
53
|
+
SnapDiff.reset
|
|
55
54
|
end
|
|
56
55
|
|
|
57
56
|
def after_teardown
|
|
@@ -67,4 +66,4 @@ module SnapDiff
|
|
|
67
66
|
end
|
|
68
67
|
end
|
|
69
68
|
|
|
70
|
-
::Minitest.after_run {
|
|
69
|
+
::Minitest.after_run { SnapDiff::Reporting.finalize! } if ::Minitest.respond_to?(:after_run)
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
require "rspec/core"
|
|
4
4
|
require_relative "../dsl"
|
|
5
5
|
# See the matching comment in integrations/minitest.rb.
|
|
6
|
-
require "
|
|
6
|
+
require "snap_diff/screenshot_assertion"
|
|
7
|
+
require "snap_diff/reporting"
|
|
7
8
|
|
|
8
9
|
RSpec::Matchers.define :match_screenshot do |name, **options|
|
|
9
10
|
description { "match screenshot '#{name}'" }
|
|
@@ -45,7 +46,7 @@ RSpec.configure do |config|
|
|
|
45
46
|
config.append_after do |example|
|
|
46
47
|
if self.class.include?(SnapDiff::DSL)
|
|
47
48
|
begin
|
|
48
|
-
|
|
49
|
+
SnapDiff.session.verify
|
|
49
50
|
|
|
50
51
|
# Never mask a real failure with a pending marker. Kept as
|
|
51
52
|
# defense-in-depth: `append_after` observes failures from plain
|
|
@@ -53,16 +54,16 @@ RSpec.configure do |config|
|
|
|
53
54
|
# so a user `append_after` registered after this gem still runs
|
|
54
55
|
# later than us — RSpec has no "run absolutely last" construct.
|
|
55
56
|
# Mitigation for such consumers: require this gem last.
|
|
56
|
-
if example.exception.nil? && (msg =
|
|
57
|
+
if example.exception.nil? && (msg = SnapDiff.pending_screenshots_message)
|
|
57
58
|
skip(msg)
|
|
58
59
|
end
|
|
59
|
-
rescue
|
|
60
|
+
rescue SnapDiff::ExpectationNotMet => e
|
|
60
61
|
raise RSpec::Expectations::ExpectationNotMetError.new(e.message).tap { |ex| ex.set_backtrace(e.backtrace) }
|
|
61
62
|
ensure
|
|
62
|
-
|
|
63
|
+
SnapDiff.reset
|
|
63
64
|
end
|
|
64
65
|
end
|
|
65
66
|
end
|
|
66
67
|
|
|
67
|
-
config.after(:suite) {
|
|
68
|
+
config.after(:suite) { SnapDiff::Reporting.finalize! }
|
|
68
69
|
end
|
|
@@ -24,6 +24,14 @@ require "snap_diff/drivers"
|
|
|
24
24
|
# shared SnapDiff::Drivers module (the Drivers alias is same-object by
|
|
25
25
|
# contract), so const_missing can never fire for the leaf names;
|
|
26
26
|
# resolving them through the old path still warns for ...::Drivers.
|
|
27
|
+
# - Diff::LOADED_DRIVERS: user code registers custom drivers by mutating
|
|
28
|
+
# this hash in place, so it must be the exact same object as the
|
|
29
|
+
# canonical SnapDiff::Drivers.loaded -- a lazy warn-once shim could not
|
|
30
|
+
# keep a mutable alias, and warning on a supported registration surface
|
|
31
|
+
# would be noise. Assigned eagerly below.
|
|
32
|
+
# - Diff::AVAILABLE_DRIVERS: stays a real constant defined by
|
|
33
|
+
# config_legacy.rb (detection runs at that load moment);
|
|
34
|
+
# SnapDiff::Drivers.available is the canonical reader.
|
|
27
35
|
module SnapDiff
|
|
28
36
|
# @api private
|
|
29
37
|
module LegacyShims
|
|
@@ -47,6 +55,11 @@ end
|
|
|
47
55
|
module Capybara
|
|
48
56
|
module Screenshot
|
|
49
57
|
module Diff
|
|
58
|
+
# EAGER same-object alias of the canonical driver cache (see header).
|
|
59
|
+
LOADED_DRIVERS = SnapDiff::Drivers.loaded
|
|
60
|
+
|
|
61
|
+
module Reporters
|
|
62
|
+
end
|
|
50
63
|
end
|
|
51
64
|
end
|
|
52
65
|
end
|
|
@@ -71,9 +84,14 @@ SnapDiff::LegacyShims.install(Capybara::Screenshot::Diff, "Capybara::Screenshot:
|
|
|
71
84
|
ScreenshotMatcher: "SnapDiff::ScreenshotMatcher",
|
|
72
85
|
Drivers: "SnapDiff::Drivers",
|
|
73
86
|
ImageCompare: "SnapDiff::Comparison",
|
|
87
|
+
Comparison: "SnapDiff::Comparison::Images",
|
|
74
88
|
Difference: "SnapDiff::ComparisonResult"
|
|
75
89
|
}.freeze)
|
|
76
90
|
|
|
91
|
+
SnapDiff::LegacyShims.install(Capybara::Screenshot::Diff::Reporters, "Capybara::Screenshot::Diff::Reporters", {
|
|
92
|
+
Default: "SnapDiff::Reporters::Default"
|
|
93
|
+
}.freeze)
|
|
94
|
+
|
|
77
95
|
SnapDiff::LegacyShims.install(CapybaraScreenshotDiff, "CapybaraScreenshotDiff", {
|
|
78
96
|
RED_RGBA: "SnapDiff::RED_RGBA",
|
|
79
97
|
ORANGE_RGBA: "SnapDiff::ORANGE_RGBA",
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SnapDiff
|
|
4
|
+
class Region
|
|
5
|
+
attr_accessor :x, :y, :width, :height
|
|
6
|
+
|
|
7
|
+
def initialize(x, y, width, height)
|
|
8
|
+
@x, @y, @width, @height = x, y, width, height
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.from_edge_coordinates(left, top, right, bottom)
|
|
12
|
+
return nil unless left && top && right && bottom
|
|
13
|
+
return nil if right < left || bottom < top
|
|
14
|
+
|
|
15
|
+
Region.new(left, top, right - left, bottom - top)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def to_edge_coordinates
|
|
19
|
+
[left, top, right, bottom]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_top_left_corner_coordinates
|
|
23
|
+
[x, y, width, height]
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def top
|
|
27
|
+
y
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def bottom
|
|
31
|
+
y + height
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def left
|
|
35
|
+
x
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def right
|
|
39
|
+
x + width
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def size
|
|
43
|
+
return 0 if width < 0 || height < 0
|
|
44
|
+
|
|
45
|
+
result = width * height
|
|
46
|
+
result.zero? ? 1 : result
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def to_a
|
|
50
|
+
[@x, @y, @width, @height]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def find_intersect_with(region)
|
|
54
|
+
return nil unless intersect?(region)
|
|
55
|
+
|
|
56
|
+
new_left = [x, region.x].max
|
|
57
|
+
new_top = [y, region.y].max
|
|
58
|
+
|
|
59
|
+
Region.new(new_left, new_top, [right, region.right].min - new_left, [bottom, region.bottom].min - new_top)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def intersect?(region)
|
|
63
|
+
left <= region.right && right >= region.left && top <= region.bottom && bottom >= region.top
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def move_by(right_by, down_by)
|
|
67
|
+
Region.new(x + right_by, y + down_by, width, height)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def find_relative_intersect(region)
|
|
71
|
+
intersect = find_intersect_with(region)
|
|
72
|
+
return nil unless intersect
|
|
73
|
+
|
|
74
|
+
intersect.move_by(-x, -y)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def cover?(x, y)
|
|
78
|
+
x.between?(left, right) && y.between?(top, bottom)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def empty?
|
|
82
|
+
width.zero? || height.zero?
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def blank?
|
|
86
|
+
empty?
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def present?
|
|
90
|
+
!empty?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def inspect
|
|
94
|
+
"Region(x: #{x}, y: #{y}, width: #{width}, height: #{height})"
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# need to add this method to make it work with assert_equal
|
|
98
|
+
def ==(other)
|
|
99
|
+
case other
|
|
100
|
+
when Region
|
|
101
|
+
x == other.x && y == other.y && width == other.width && height == other.height
|
|
102
|
+
when Array
|
|
103
|
+
to_a == other
|
|
104
|
+
else
|
|
105
|
+
false
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Compatibility alias -- EAGER and same-object on purpose, mirroring the
|
|
112
|
+
# ADR-008 step 2 rationale: user configs construct skip_area entries with
|
|
113
|
+
# top-level `Region.new(...)` and SnapDiff internals check
|
|
114
|
+
# `is_a?(SnapDiff::Region)`, so both names must be the exact same class;
|
|
115
|
+
# and `defined?(Region)` feature detection must be truthy as soon as this
|
|
116
|
+
# file loads (a lazy const_missing shim would return nil there).
|
|
117
|
+
Region = SnapDiff::Region
|