snap_diff-capybara 2.0.0.beta1 → 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 +56 -0
- data/docs/UPGRADING.md +19 -4
- data/docs/architecture.md +38 -22
- 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/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
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "snap_diff/annotation_service"
|
|
4
|
+
|
|
5
|
+
module SnapDiff
|
|
6
|
+
module Reporters
|
|
7
|
+
class Default
|
|
8
|
+
attr_reader :difference
|
|
9
|
+
|
|
10
|
+
def initialize(difference)
|
|
11
|
+
@difference = difference
|
|
12
|
+
@annotation_service = SnapDiff::AnnotationService.new(difference)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def annotated_image_path
|
|
16
|
+
annotation_service.annotated_image_path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def annotated_base_image_path
|
|
20
|
+
annotation_service.annotated_base_image_path
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def heatmap_diff_path
|
|
24
|
+
annotation_service.heatmap_diff_path
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def generate
|
|
28
|
+
if difference.equal?
|
|
29
|
+
# NOTE: Delete previous run runtime files
|
|
30
|
+
clean_tmp_files
|
|
31
|
+
return nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
if difference.failed? && difference.failed_by[:different_dimensions]
|
|
35
|
+
return build_error_for_different_dimensions
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
annotate_and_save_images
|
|
39
|
+
build_error_message
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def clean_tmp_files
|
|
43
|
+
annotation_service.clean_tmp_files
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def annotate_and_save_images
|
|
47
|
+
annotation_service.annotate_and_save_images
|
|
48
|
+
end
|
|
49
|
+
|
|
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
|
+
def build_error_for_different_dimensions
|
|
67
|
+
change_msg = [comparison.base_image, comparison.new_image]
|
|
68
|
+
.map { |image| driver.dimension(image).join("x") }
|
|
69
|
+
.join(" => ")
|
|
70
|
+
|
|
71
|
+
"Dimensions have changed: #{change_msg}\n#{base_image_path.to_path}\n#{image_path.to_path}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
NEW_LINE = "\n"
|
|
75
|
+
|
|
76
|
+
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)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
attr_reader :annotation_service
|
|
89
|
+
|
|
90
|
+
def base_image_path
|
|
91
|
+
comparison.base_image_path
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def image_path
|
|
95
|
+
comparison.new_image_path
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def driver
|
|
99
|
+
@_driver ||= comparison.driver
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def comparison
|
|
103
|
+
@_comparison ||= difference.comparison
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -5,11 +5,9 @@ require "erb"
|
|
|
5
5
|
require "fileutils"
|
|
6
6
|
require "pathname"
|
|
7
7
|
require "json"
|
|
8
|
-
# The auto-registration block at the bottom of this file
|
|
9
|
-
#
|
|
10
|
-
|
|
11
|
-
# file (deliberately not moved -- see that file's own comment).
|
|
12
|
-
require "capybara_screenshot_diff/screenshot_assertion"
|
|
8
|
+
# The auto-registration block at the bottom of this file registers with
|
|
9
|
+
# SnapDiff::Reporting.
|
|
10
|
+
require "snap_diff/reporting"
|
|
13
11
|
require "capybara/screenshot/diff/config_legacy"
|
|
14
12
|
|
|
15
13
|
module SnapDiff
|
|
@@ -136,8 +134,8 @@ module SnapDiff
|
|
|
136
134
|
end
|
|
137
135
|
|
|
138
136
|
# Auto-register reporter.
|
|
139
|
-
# Framework adapters (Minitest, RSpec, Cucumber) call
|
|
140
|
-
# For custom frameworks, call
|
|
141
|
-
unless
|
|
142
|
-
|
|
137
|
+
# Framework adapters (Minitest, RSpec, Cucumber) call SnapDiff::Reporting.finalize! via native hooks.
|
|
138
|
+
# For custom frameworks, call SnapDiff::Reporting.finalize! manually.
|
|
139
|
+
unless SnapDiff::Reporting.reporters.any?(SnapDiff::Reporters::HTML)
|
|
140
|
+
SnapDiff::Reporting.register(SnapDiff::Reporters::HTML.new(embed_images: !!ENV["CI"]))
|
|
143
141
|
end
|
data/lib/snap_diff/reporting.rb
CHANGED
|
@@ -5,10 +5,9 @@ module SnapDiff
|
|
|
5
5
|
# end-of-suite finalization. One list of reporters for the whole process,
|
|
6
6
|
# guarded by one mutex.
|
|
7
7
|
#
|
|
8
|
-
# Deliberately separate from the per-test session lifecycle
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# the registry does not. CapybaraScreenshotDiff keeps its public
|
|
8
|
+
# Deliberately separate from the per-test session lifecycle
|
|
9
|
+
# (SnapDiff.session): reporters outlive any single test, the session does
|
|
10
|
+
# not. CapybaraScreenshotDiff keeps its public
|
|
12
11
|
# reporters/reporters_mutex/finalize_reporters! methods as thin shims
|
|
13
12
|
# over this module.
|
|
14
13
|
module Reporting
|
|
@@ -18,6 +17,16 @@ module SnapDiff
|
|
|
18
17
|
class << self
|
|
19
18
|
attr_reader :reporters, :mutex
|
|
20
19
|
|
|
20
|
+
# Registers a reporter for the rest of the process. The canonical way
|
|
21
|
+
# in: the append happens under the mutex, so concurrent registrations
|
|
22
|
+
# cannot lose one (issue #217 item 2). `reporters` stays public and
|
|
23
|
+
# mutable for compatibility -- appending to it directly still works,
|
|
24
|
+
# it just skips the lock.
|
|
25
|
+
def register(reporter)
|
|
26
|
+
@mutex.synchronize { @reporters << reporter }
|
|
27
|
+
reporter
|
|
28
|
+
end
|
|
29
|
+
|
|
21
30
|
# Delivers a finished test's assertions to every registered reporter.
|
|
22
31
|
# Iterates over a snapshot so a reporter mutating the list mid-notify
|
|
23
32
|
# cannot affect the current round. A raising reporter is warned about
|
|
@@ -2,8 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require_relative "screenshot_namer"
|
|
5
|
+
require_relative "reporting"
|
|
5
6
|
|
|
6
7
|
module SnapDiff
|
|
8
|
+
# --- Session lifecycle (per-test) ---
|
|
9
|
+
#
|
|
10
|
+
# The canonical home of the per-test session: the AssertionRegistry
|
|
11
|
+
# holding the assertions and new-screenshot names of the test running
|
|
12
|
+
# here. CapybaraScreenshotDiff.registry / .reset /
|
|
13
|
+
# .pending_screenshots_message are thin forwarders over these.
|
|
14
|
+
#
|
|
15
|
+
# Note: Thread.current[] is *fiber*-local, so a session is per fiber, not
|
|
16
|
+
# per thread. That is pre-existing, documented behaviour (issue #217);
|
|
17
|
+
# ADR-008 step 6 relocates the accessor without touching the semantics.
|
|
18
|
+
def self.session
|
|
19
|
+
Thread.current[:capybara_screenshot_diff_registry] ||= AssertionRegistry.new
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Ends a test: hands the finished session's assertions to the reporters,
|
|
23
|
+
# then clears it. The one deliberate bridge between the process-global
|
|
24
|
+
# reporter lifecycle (SnapDiff::Reporting) and the per-test session.
|
|
25
|
+
def self.reset
|
|
26
|
+
Reporting.notify(session.assertions)
|
|
27
|
+
session.reset
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Message to skip the test with when a new screenshot has no baseline yet
|
|
31
|
+
# and `pending_if_new` is enabled. Adapters call this after verifying
|
|
32
|
+
# screenshots, and skip the test with the returned message when present.
|
|
33
|
+
#
|
|
34
|
+
# @return [String, nil] the pending message, or nil when there is nothing to report
|
|
35
|
+
def self.pending_screenshots_message
|
|
36
|
+
return unless ::Capybara::Screenshot::Diff.pending_if_new && session.new_screenshots_present?
|
|
37
|
+
|
|
38
|
+
"No baseline for: #{session.new_screenshots.join(", ")}. Commit the captured screenshots to record them."
|
|
39
|
+
end
|
|
40
|
+
|
|
7
41
|
class ScreenshotAssertion
|
|
8
42
|
attr_reader :name, :args
|
|
9
43
|
attr_accessor :compare, :caller
|
|
@@ -52,7 +86,7 @@ module SnapDiff
|
|
|
52
86
|
error_msg = validate
|
|
53
87
|
|
|
54
88
|
if error_msg
|
|
55
|
-
raise
|
|
89
|
+
raise SnapDiff::ExpectationNotMet.new(error_msg, caller)
|
|
56
90
|
end
|
|
57
91
|
end
|
|
58
92
|
|
|
@@ -118,15 +152,14 @@ module SnapDiff
|
|
|
118
152
|
!@new_screenshots.empty?
|
|
119
153
|
end
|
|
120
154
|
|
|
121
|
-
def verify(screenshots =
|
|
155
|
+
def verify(screenshots = assertions)
|
|
122
156
|
return unless ::Capybara::Screenshot.active? && ::Capybara::Screenshot::Diff.fail_on_difference
|
|
123
157
|
|
|
124
|
-
failed_assertions = CapybaraScreenshotDiff.registry.failed_assertions
|
|
125
158
|
failed_screenshot = failed_assertions.first
|
|
126
159
|
result = ScreenshotAssertion.verify_screenshots!(screenshots)
|
|
127
160
|
|
|
128
161
|
if result
|
|
129
|
-
raise
|
|
162
|
+
raise SnapDiff::ExpectationNotMet.new(result.join("\n\n"), failed_screenshot.caller)
|
|
130
163
|
end
|
|
131
164
|
end
|
|
132
165
|
|
|
@@ -21,7 +21,7 @@ module SnapDiff
|
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def build_screenshot_assertion(skip_stack_frames: 0)
|
|
24
|
-
Capture::Viewport.prepare!(Capybara::Screenshot.window_size
|
|
24
|
+
Capture::Viewport.prepare!(Capybara::Screenshot.window_size)
|
|
25
25
|
prepare_screenshot_options
|
|
26
26
|
check_base_screenshot
|
|
27
27
|
|
|
@@ -32,7 +32,7 @@ module SnapDiff
|
|
|
32
32
|
# Pre-computation: No need to compare without base screenshot
|
|
33
33
|
# NOTE: Consider to return PreValid Assertion Value Object with hard coded valid result
|
|
34
34
|
unless need_to_compare?
|
|
35
|
-
|
|
35
|
+
SnapDiff.session.record_new_screenshot(screenshot_full_name)
|
|
36
36
|
return
|
|
37
37
|
end
|
|
38
38
|
|
|
@@ -41,7 +41,7 @@ module SnapDiff
|
|
|
41
41
|
|
|
42
42
|
# Captures a screenshot without comparing it to a baseline.
|
|
43
43
|
def capture
|
|
44
|
-
Capture::Viewport.prepare!(Capybara::Screenshot.window_size
|
|
44
|
+
Capture::Viewport.prepare!(Capybara::Screenshot.window_size)
|
|
45
45
|
prepare_screenshot_options
|
|
46
46
|
|
|
47
47
|
capture_options, comparison_options = extract_capture_and_comparison_options(driver_options)
|
|
@@ -68,7 +68,7 @@ module SnapDiff
|
|
|
68
68
|
@snapshot.checkout_base_screenshot
|
|
69
69
|
|
|
70
70
|
if Capybara::Screenshot::Diff.fail_if_new && !@snapshot.base_path.exist?
|
|
71
|
-
raise
|
|
71
|
+
raise SnapDiff::ExpectationNotMet.new(<<~ERROR.chomp, caller)
|
|
72
72
|
No existing screenshot found for #{@snapshot.base_path}!
|
|
73
73
|
To record baselines: RECORD_SCREENSHOTS=1 bundle exec rake test
|
|
74
74
|
To allow new screenshots: Capybara::Screenshot::Diff.fail_if_new = false
|
|
@@ -88,7 +88,7 @@ module SnapDiff
|
|
|
88
88
|
break unless pending_image
|
|
89
89
|
|
|
90
90
|
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline_at
|
|
91
|
-
raise
|
|
91
|
+
raise SnapDiff::ExpectationNotMet.new("Images have not been loaded after #{timeout}s: #{pending_image.inspect}", caller)
|
|
92
92
|
end
|
|
93
93
|
|
|
94
94
|
sleep 0.025
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
|
|
5
5
|
require_relative "vcs"
|
|
6
|
-
require "active_support/core_ext/module/attribute_accessors"
|
|
7
6
|
|
|
8
7
|
require_relative "snap"
|
|
9
8
|
|
|
@@ -100,7 +99,7 @@ module SnapDiff
|
|
|
100
99
|
# manager class or screenshot root changes (tests re-root per example).
|
|
101
100
|
#
|
|
102
101
|
# Thread.current[] is fiber-local — same construct as
|
|
103
|
-
#
|
|
102
|
+
# SnapDiff.session; both split per fiber and must migrate
|
|
104
103
|
# together if async support ever lands.
|
|
105
104
|
#
|
|
106
105
|
# Scope of the fix: default-root unit tests and before_setup re-rooters
|