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,27 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "snap_diff/removal"
|
|
4
|
+
require "snap_diff/snap_manager"
|
|
4
5
|
require_relative "screenshoter"
|
|
5
6
|
require_relative "stable_screenshoter"
|
|
6
7
|
require_relative "browser_helpers"
|
|
7
8
|
require_relative "capture/viewport"
|
|
8
9
|
require_relative "vcs"
|
|
9
10
|
require_relative "area_calculator"
|
|
11
|
+
require_relative "reporting"
|
|
10
12
|
|
|
11
13
|
module SnapDiff
|
|
12
14
|
class ScreenshotMatcher
|
|
13
|
-
attr_reader :screenshot_full_name, :driver_options, :screenshot_format
|
|
15
|
+
attr_reader :screenshot_full_name, :driver_options, :screenshot_format, :record_mode
|
|
14
16
|
|
|
15
17
|
def initialize(screenshot_full_name, options = {})
|
|
16
18
|
@screenshot_full_name = screenshot_full_name
|
|
17
|
-
|
|
19
|
+
# BEFORE the merge below -- see SnapDiff.compare. Afterwards `:driver`
|
|
20
|
+
# is present for every caller and proves nothing.
|
|
21
|
+
Removal.warn_once(:driver_setting, Removal::DRIVER_REMOVED) if options.key?(:driver)
|
|
22
|
+
@driver_options = SnapDiff.config.default_options.merge(options)
|
|
23
|
+
|
|
24
|
+
# `record:` is a workflow mode, not a capture or comparison option, so
|
|
25
|
+
# it is carved out here rather than added to Comparison::KNOWN_OPTIONS
|
|
26
|
+
# -- a key that hash accepts and nothing downstream reads is the
|
|
27
|
+
# silent no-op ADR-010 forbids. Deleted before anything else touches
|
|
28
|
+
# the hash, so no later split has to know about it.
|
|
29
|
+
@record_mode = resolve_record_mode(@driver_options.delete(:record))
|
|
18
30
|
|
|
19
31
|
@screenshot_format = @driver_options[:screenshot_format]
|
|
20
32
|
@snapshot = SnapDiff::SnapManager.snapshot(screenshot_full_name, @screenshot_format)
|
|
21
33
|
end
|
|
22
34
|
|
|
23
35
|
def build_screenshot_assertion(skip_stack_frames: 0)
|
|
24
|
-
|
|
36
|
+
# Here rather than in #initialize, so it covers exactly the path where
|
|
37
|
+
# `:all` means anything. #capture never compares against a baseline, so
|
|
38
|
+
# the mode has nothing to say about it and refusing there would be a
|
|
39
|
+
# failure invented out of a setting that changes nothing.
|
|
40
|
+
refuse_bulk_record_under_ci! if record_mode == :all
|
|
41
|
+
|
|
42
|
+
Capture::Viewport.prepare!(SnapDiff.config.window_size)
|
|
25
43
|
prepare_screenshot_options
|
|
26
44
|
check_base_screenshot
|
|
27
45
|
|
|
@@ -29,10 +47,14 @@ module SnapDiff
|
|
|
29
47
|
|
|
30
48
|
capture_screenshot(capture_options, comparison_options)
|
|
31
49
|
|
|
50
|
+
# AFTER the capture, so the path the message tells the user to
|
|
51
|
+
# `git add` is one that exists by the time they read it (#260).
|
|
52
|
+
fail_if_new_screenshot
|
|
53
|
+
|
|
32
54
|
# Pre-computation: No need to compare without base screenshot
|
|
33
55
|
# NOTE: Consider to return PreValid Assertion Value Object with hard coded valid result
|
|
34
56
|
unless need_to_compare?
|
|
35
|
-
|
|
57
|
+
record_uncompared_screenshot
|
|
36
58
|
return
|
|
37
59
|
end
|
|
38
60
|
|
|
@@ -41,7 +63,7 @@ module SnapDiff
|
|
|
41
63
|
|
|
42
64
|
# Captures a screenshot without comparing it to a baseline.
|
|
43
65
|
def capture
|
|
44
|
-
Capture::Viewport.prepare!(
|
|
66
|
+
Capture::Viewport.prepare!(SnapDiff.config.window_size)
|
|
45
67
|
prepare_screenshot_options
|
|
46
68
|
|
|
47
69
|
capture_options, comparison_options = extract_capture_and_comparison_options(driver_options)
|
|
@@ -52,10 +74,53 @@ module SnapDiff
|
|
|
52
74
|
|
|
53
75
|
private
|
|
54
76
|
|
|
77
|
+
# The per-screenshot option outranks the configured mode, which in turn
|
|
78
|
+
# outranks `fail_if_new` (SnapDiff::Config#record). One resolution, so
|
|
79
|
+
# both routes to a mode agree about everything downstream.
|
|
80
|
+
def resolve_record_mode(per_screenshot)
|
|
81
|
+
SnapDiff::Config.validate_record_mode!(per_screenshot) || SnapDiff.config.record
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# `:all` accepts every rendering as correct -- that is the feature. On
|
|
85
|
+
# CI there is nobody to review the result and the re-recorded files go
|
|
86
|
+
# away with the box, so a mode left in a committed config file buys a
|
|
87
|
+
# build that compares nothing and passes, forever. That is precisely how
|
|
88
|
+
# Percy goes green on a job that lost its token; refuse it rather than
|
|
89
|
+
# shipping our own version.
|
|
90
|
+
#
|
|
91
|
+
# A CI job that must record NEW baselines does not need `:all` at all:
|
|
92
|
+
# `record: :once` records them and still compares everything that has a
|
|
93
|
+
# baseline (docs/ci-integration.md).
|
|
94
|
+
def refuse_bulk_record_under_ci!
|
|
95
|
+
return if ENV["CI"].to_s.empty?
|
|
96
|
+
|
|
97
|
+
raise SnapDiff::ExpectationNotMet.new(<<~ERROR.chomp, caller)
|
|
98
|
+
`record: :all` re-records every baseline WITHOUT comparing, so it refuses to run under CI (ENV["CI"] is set).
|
|
99
|
+
Nothing would be verified and the recorded screenshots would be discarded with the runner.
|
|
100
|
+
Re-record locally, review the result, and commit the screenshots.
|
|
101
|
+
To let a CI job record screenshots that have no baseline yet: SnapDiff.config.record = :once
|
|
102
|
+
ERROR
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# No `record_mode != :all` clause here on purpose: mutation testing
|
|
106
|
+
# showed one guards nothing. #check_base_screenshot already leaves `:all`
|
|
107
|
+
# with no base file at all, so this reads false for it either way, and a
|
|
108
|
+
# second condition that cannot fire is a second thing to keep true.
|
|
55
109
|
def need_to_compare?
|
|
56
110
|
@snapshot.base_path.exist?
|
|
57
111
|
end
|
|
58
112
|
|
|
113
|
+
# `:all` re-records, so nothing was compared and nothing is "new" in the
|
|
114
|
+
# missing-baseline sense. Reported through its own channel, which counts
|
|
115
|
+
# only the screenshots that really went down this path.
|
|
116
|
+
def record_uncompared_screenshot
|
|
117
|
+
if record_mode == :all
|
|
118
|
+
SnapDiff::Reporting.record_rerecorded_baseline(screenshot_full_name)
|
|
119
|
+
else
|
|
120
|
+
SnapDiff.session.record_new_screenshot(screenshot_full_name)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
59
124
|
def prepare_screenshot_options
|
|
60
125
|
area_calculator = AreaCalculator.new(driver_options.delete(:crop), driver_options[:skip_area])
|
|
61
126
|
|
|
@@ -64,23 +129,65 @@ module SnapDiff
|
|
|
64
129
|
driver_options[:driver] = SnapDiff::Drivers.for(driver_options[:driver])
|
|
65
130
|
end
|
|
66
131
|
|
|
132
|
+
# The git checkout that drives #need_to_compare?, plus the half of the
|
|
133
|
+
# no-baseline reporting that MUST run before the capture: it is the only
|
|
134
|
+
# moment at which `@snapshot.path` still tells us whether the user had a
|
|
135
|
+
# PNG sitting there already -- the case that confuses people most.
|
|
67
136
|
def check_base_screenshot
|
|
137
|
+
# `:all` does not ask git for a baseline -- there is nothing to
|
|
138
|
+
# compare against. A `.base.<fmt>` left by an earlier failing run
|
|
139
|
+
# would otherwise sit beside the re-recorded screenshot and land in
|
|
140
|
+
# the user's `git add`.
|
|
141
|
+
return discard_base_screenshot if record_mode == :all
|
|
142
|
+
|
|
68
143
|
@snapshot.checkout_base_screenshot
|
|
144
|
+
return if @snapshot.base_path.exist?
|
|
145
|
+
# fail_if_new_screenshot raises after the capture and says the same
|
|
146
|
+
# thing with the fix attached; two messages for one missing baseline
|
|
147
|
+
# is one too many.
|
|
148
|
+
return if record_mode == :none
|
|
69
149
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
150
|
+
warn_no_committed_baseline
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def discard_base_screenshot
|
|
154
|
+
@snapshot.base_path.delete if @snapshot.base_path.exist?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Runs AFTER the capture, so `@snapshot.path` names a file that is
|
|
158
|
+
# really there and `git add` on it is a command the user can run on this
|
|
159
|
+
# very test run (#260). Before, the raise came first and the screenshot
|
|
160
|
+
# was never written -- the instruction was unfollowable in CI, the one
|
|
161
|
+
# place fail_if_new is on by default.
|
|
162
|
+
def fail_if_new_screenshot
|
|
163
|
+
return if @snapshot.base_path.exist?
|
|
164
|
+
return unless record_mode == :none
|
|
165
|
+
|
|
166
|
+
raise SnapDiff::ExpectationNotMet.new(<<~ERROR.chomp, caller)
|
|
167
|
+
No existing screenshot found for #{@snapshot.path}!
|
|
168
|
+
To record it: `git add #{@snapshot.path}` and commit -- baselines are read from git.
|
|
169
|
+
To allow new screenshots: SnapDiff.config.record = :once (was: SnapDiff.config.fail_if_new = false)
|
|
170
|
+
ERROR
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# `fail_if_new` defaults to false off CI, deliberately: a new screenshot
|
|
174
|
+
# must not break a local run. The cost is that nothing is compared and
|
|
175
|
+
# the test passes whatever the page looks like, while the capture
|
|
176
|
+
# overwrites the file on disk -- a green run that proves nothing. Say so
|
|
177
|
+
# once per screenshot, and name what to do about it.
|
|
178
|
+
def warn_no_committed_baseline
|
|
179
|
+
return unless SnapDiff::Reporting.record_missing_baseline(screenshot_full_name)
|
|
180
|
+
|
|
181
|
+
already_there = @snapshot.path.exist? ? " (the file already there is not a baseline until it is committed)" : ""
|
|
182
|
+
warn "[snap_diff] No committed baseline for #{@snapshot.path}#{already_there} -- nothing was compared. " \
|
|
183
|
+
"Commit it to enable comparison."
|
|
77
184
|
end
|
|
78
185
|
|
|
79
186
|
def capture_screenshot(capture_options, comparison_options)
|
|
80
187
|
screenshoter = if capture_options[:stability_time_limit]
|
|
81
188
|
StableScreenshoter.new(capture_options, comparison_options)
|
|
82
189
|
else
|
|
83
|
-
|
|
190
|
+
SnapDiff.config.screenshoter.new(capture_options, comparison_options)
|
|
84
191
|
end
|
|
85
192
|
screenshoter.take_comparison_screenshot(@snapshot)
|
|
86
193
|
end
|
|
@@ -11,15 +11,10 @@ module SnapDiff
|
|
|
11
11
|
class ScreenshotNamer
|
|
12
12
|
attr_reader :section, :group
|
|
13
13
|
|
|
14
|
-
def initialize
|
|
14
|
+
def initialize
|
|
15
15
|
@section = nil
|
|
16
16
|
@group = nil
|
|
17
17
|
@counter = nil
|
|
18
|
-
@screenshot_area = screenshot_area
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def screenshot_area
|
|
22
|
-
@screenshot_area ||= Capybara::Screenshot.screenshot_area
|
|
23
18
|
end
|
|
24
19
|
|
|
25
20
|
# Sets the current section for screenshots.
|
|
@@ -50,13 +45,6 @@ module SnapDiff
|
|
|
50
45
|
File.join(*directory_parts.push(name.to_s))
|
|
51
46
|
end
|
|
52
47
|
|
|
53
|
-
# Builds the full path for a screenshot file, including section and group directories.
|
|
54
|
-
# @param base_name [String] The base name for the screenshot.
|
|
55
|
-
# @return [String] The absolute path for the screenshot file.
|
|
56
|
-
def full_name_with_path(base_name)
|
|
57
|
-
File.join(screenshot_area, full_name(base_name))
|
|
58
|
-
end
|
|
59
|
-
|
|
60
48
|
# Returns the directory parts (section and group) for constructing paths.
|
|
61
49
|
# @return [Array<String>] An array of directory names.
|
|
62
50
|
def directory_parts
|
|
@@ -66,12 +54,6 @@ module SnapDiff
|
|
|
66
54
|
parts
|
|
67
55
|
end
|
|
68
56
|
|
|
69
|
-
# Calculates the directory path for the current section and group.
|
|
70
|
-
# @return [String] The full path to the directory.
|
|
71
|
-
def current_group_directory
|
|
72
|
-
File.join(*([screenshot_area] + directory_parts))
|
|
73
|
-
end
|
|
74
|
-
|
|
75
57
|
private
|
|
76
58
|
|
|
77
59
|
def reset_group_counter
|
|
@@ -51,9 +51,7 @@ module SnapDiff
|
|
|
51
51
|
screenshot_image = driver.from_file(stored_path)
|
|
52
52
|
|
|
53
53
|
# TODO(uwe): Remove when chromedriver takes right size screenshots
|
|
54
|
-
# TODO: Adds tests when this case is true
|
|
55
54
|
screenshot_image = resize_if_needed(screenshot_image) if selenium_with_retina_screen?
|
|
56
|
-
# ODOT
|
|
57
55
|
|
|
58
56
|
screenshot_image = driver.crop(crop, screenshot_image) if crop
|
|
59
57
|
|
|
@@ -71,10 +69,10 @@ module SnapDiff
|
|
|
71
69
|
def prepare_page_for_screenshot(timeout:)
|
|
72
70
|
wait_images_loaded(timeout: timeout) if timeout
|
|
73
71
|
|
|
74
|
-
blurred_input = BrowserHelpers.blur_from_focused_element if
|
|
72
|
+
blurred_input = BrowserHelpers.blur_from_focused_element if SnapDiff.config.blur_active_element
|
|
75
73
|
|
|
76
|
-
BrowserHelpers.hide_caret if
|
|
77
|
-
BrowserHelpers.disable_animations if
|
|
74
|
+
BrowserHelpers.hide_caret if SnapDiff.config.hide_caret
|
|
75
|
+
BrowserHelpers.disable_animations if SnapDiff.config.disable_animations
|
|
78
76
|
|
|
79
77
|
blurred_input
|
|
80
78
|
end
|
|
@@ -113,7 +111,7 @@ module SnapDiff
|
|
|
113
111
|
end
|
|
114
112
|
|
|
115
113
|
def resize_if_needed(saved_image)
|
|
116
|
-
expected_image_width =
|
|
114
|
+
expected_image_width = SnapDiff.config.window_size[0]
|
|
117
115
|
return saved_image if driver.width_for(saved_image) < expected_image_width * 2
|
|
118
116
|
|
|
119
117
|
notice_how_to_avoid_this
|
|
@@ -123,7 +121,7 @@ module SnapDiff
|
|
|
123
121
|
end
|
|
124
122
|
|
|
125
123
|
def selenium_with_retina_screen?
|
|
126
|
-
Os::ON_MAC && BrowserHelpers.selenium? &&
|
|
124
|
+
Os::ON_MAC && BrowserHelpers.selenium? && SnapDiff.config.window_size
|
|
127
125
|
end
|
|
128
126
|
end
|
|
129
127
|
end
|
data/lib/snap_diff/snap.rb
CHANGED
|
@@ -20,8 +20,13 @@ module SnapDiff
|
|
|
20
20
|
cleanup_attempts!
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# Records the successful checkout on the session, so that a LATER
|
|
24
|
+
# "there is no baseline" reading can be told apart from "there never
|
|
25
|
+
# was one". Only the second is a legitimate state (#217).
|
|
23
26
|
def checkout_base_screenshot
|
|
24
|
-
@manager.checkout_file(path, base_path)
|
|
27
|
+
@manager.checkout_file(path, base_path).tap do |checked_out|
|
|
28
|
+
SnapDiff.session.record_baseline_checkout(full_name) if checked_out
|
|
29
|
+
end
|
|
25
30
|
end
|
|
26
31
|
|
|
27
32
|
def path_for(version = :actual)
|
|
@@ -55,7 +55,6 @@ module SnapDiff
|
|
|
55
55
|
path ? path.dirname.mkpath : root.mkpath
|
|
56
56
|
end
|
|
57
57
|
|
|
58
|
-
# TODO: rename to delete!
|
|
59
58
|
def cleanup!
|
|
60
59
|
snapshots.each do |snapshot|
|
|
61
60
|
cleanup_attempts!(snapshot)
|
|
@@ -111,8 +110,8 @@ module SnapDiff
|
|
|
111
110
|
# test/fixtures/app/doc/screenshots/ that VCS rollback just restored.
|
|
112
111
|
# Do not reorder those teardowns without revisiting this.
|
|
113
112
|
def self.instance
|
|
114
|
-
manager_class =
|
|
115
|
-
root = Pathname.new(
|
|
113
|
+
manager_class = SnapDiff.config.manager
|
|
114
|
+
root = Pathname.new(SnapDiff.config.screenshot_area_abs)
|
|
116
115
|
|
|
117
116
|
current = Thread.current[:snap_diff_manager]
|
|
118
117
|
unless current&.instance_of?(manager_class) && current.root == root
|
|
@@ -12,7 +12,7 @@ module SnapDiff
|
|
|
12
12
|
# `:stability_time_limit` and `:wait` in capture options and ensures that `:stability_time_limit` is less than or equal to `:wait`.
|
|
13
13
|
#
|
|
14
14
|
# @param capture_options [Hash] The options for capturing screenshots, must include `:stability_time_limit` and `:wait`.
|
|
15
|
-
# @param comparison_options [Hash] The options for comparing screenshots, defaults to `{}`. Same signature as {
|
|
15
|
+
# @param comparison_options [Hash] The options for comparing screenshots, defaults to `{}`. Same signature as {SnapDiff::Screenshoter#initialize}.
|
|
16
16
|
# @raise [ArgumentError] If `:wait` or `:stability_time_limit` are not provided, or if `:stability_time_limit` is greater than `:wait`.
|
|
17
17
|
def initialize(capture_options, comparison_options = {})
|
|
18
18
|
@stability_time_limit, @wait = capture_options.fetch_values(*STABILITY_OPTIONS)
|
|
@@ -23,7 +23,7 @@ module SnapDiff
|
|
|
23
23
|
|
|
24
24
|
@comparison_options = comparison_options
|
|
25
25
|
|
|
26
|
-
@screenshoter =
|
|
26
|
+
@screenshoter = SnapDiff.config.screenshoter.new(capture_options.except(:stability_time_limit), @comparison_options)
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
# Takes a comparison screenshot ensuring page stability
|
data/lib/snap_diff/static.rb
CHANGED
data/lib/snap_diff/utils.rb
CHANGED
|
@@ -1,30 +1,48 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "snap_diff/drivers"
|
|
4
|
+
require "snap_diff/removal"
|
|
4
5
|
|
|
5
6
|
module SnapDiff
|
|
6
7
|
module Utils
|
|
8
|
+
# THE selection funnel. Every surface that picks a driver ends up here --
|
|
9
|
+
# `driver: :chunky_png` per comparison, `SnapDiff.config.driver =`, the
|
|
10
|
+
# legacy `Diff.driver =` (a delegator onto the same storage), and `:auto`
|
|
11
|
+
# -- so this is the one place the chunky_png removal has to be announced
|
|
12
|
+
# from. Warned before the registry lookup, not inside it: the cache is a
|
|
13
|
+
# `||=`, so a hook there would fire only for the first comparison of a
|
|
14
|
+
# process that happened to miss.
|
|
15
|
+
CHUNKY_PNG_REMOVED =
|
|
16
|
+
"The chunky_png driver is REMOVED in 2.1, when libvips (the `ruby-vips` gem) becomes " \
|
|
17
|
+
"required. Install ruby-vips and drop `driver: :chunky_png`. See docs/drivers.md."
|
|
18
|
+
|
|
19
|
+
# The case that matters most: nobody asked for this driver, so the
|
|
20
|
+
# warning has to say why they are on it.
|
|
21
|
+
CHUNKY_PNG_AUTO_REMOVED =
|
|
22
|
+
"`driver: :auto` selected chunky_png because libvips is not available in this process. " \
|
|
23
|
+
"The chunky_png driver is REMOVED in 2.1, when libvips (the `ruby-vips` gem) becomes " \
|
|
24
|
+
"required -- install it now, or this setup stops comparing on 2.1. See docs/drivers.md."
|
|
25
|
+
|
|
26
|
+
# Detection itself lives on Drivers now (its canonical home -- so that
|
|
27
|
+
# `require "snap_diff/drivers"` standalone can answer .available); this
|
|
28
|
+
# keeps the documented Utils name working. One-way: Drivers never calls
|
|
29
|
+
# back here at load time, so requiring either file first is safe.
|
|
7
30
|
def self.detect_available_drivers
|
|
8
|
-
|
|
9
|
-
begin
|
|
10
|
-
result << :vips if defined?(Vips) || require("vips")
|
|
11
|
-
rescue LoadError
|
|
12
|
-
# vips not present
|
|
13
|
-
Object.send(:remove_const, :Vips) if defined?(Vips)
|
|
14
|
-
end
|
|
15
|
-
begin
|
|
16
|
-
result << :chunky_png if defined?(ChunkyPNG) || require("chunky_png")
|
|
17
|
-
rescue LoadError
|
|
18
|
-
# chunky_png not present
|
|
19
|
-
Object.send(:remove_const, :ChunkyPNG) if defined?(ChunkyPNG)
|
|
20
|
-
end
|
|
21
|
-
result
|
|
31
|
+
Drivers.detect_available
|
|
22
32
|
end
|
|
23
33
|
|
|
24
34
|
def self.find_driver_class_for(driver)
|
|
25
|
-
|
|
35
|
+
if driver == :auto
|
|
36
|
+
# Drivers::AVAILABLE_DRIVERS, not Drivers.available: same value and
|
|
37
|
+
# the same stubbing point, without the gem tripping .available's own
|
|
38
|
+
# removal warning on every comparison.
|
|
39
|
+
driver = Drivers::AVAILABLE_DRIVERS.first
|
|
40
|
+
Removal.warn_once(:chunky_png_auto, CHUNKY_PNG_AUTO_REMOVED) if driver == :chunky_png
|
|
41
|
+
elsif driver == :chunky_png
|
|
42
|
+
Removal.warn_once(:chunky_png, CHUNKY_PNG_REMOVED)
|
|
43
|
+
end
|
|
26
44
|
|
|
27
|
-
Drivers.
|
|
45
|
+
Drivers.registry[driver] ||=
|
|
28
46
|
case driver
|
|
29
47
|
when :chunky_png
|
|
30
48
|
require "snap_diff/drivers/chunky_png_driver"
|
|
@@ -33,7 +51,7 @@ module SnapDiff
|
|
|
33
51
|
require "snap_diff/drivers/vips_driver"
|
|
34
52
|
SnapDiff::Drivers::VipsDriver
|
|
35
53
|
else
|
|
36
|
-
fail "Wrong adapter #{driver.inspect}. Available adapters: #{Drivers.
|
|
54
|
+
fail "Wrong adapter #{driver.inspect}. Available adapters: #{Drivers::AVAILABLE_DRIVERS.inspect}"
|
|
37
55
|
end
|
|
38
56
|
end
|
|
39
57
|
end
|
data/lib/snap_diff/vcs.rb
CHANGED
|
@@ -5,23 +5,32 @@ require_relative "os"
|
|
|
5
5
|
|
|
6
6
|
module SnapDiff
|
|
7
7
|
module Vcs
|
|
8
|
+
# `-C <dir>` sets the working directory, but GIT_DIR/GIT_WORK_TREE OVERRIDE
|
|
9
|
+
# it -- so a suite launched from a git hook (which exports both) reads the
|
|
10
|
+
# WRONG repository. Every baseline lookup then fails, and because
|
|
11
|
+
# `fail_if_new` is false locally, screenshots are recorded as new and the
|
|
12
|
+
# tests PASS. Scrubbing them makes `-C` mean what this code already assumes.
|
|
13
|
+
GIT_ENV = {"GIT_DIR" => nil, "GIT_WORK_TREE" => nil, "GIT_INDEX_FILE" => nil}.freeze
|
|
14
|
+
|
|
15
|
+
@git_roots = {}
|
|
16
|
+
@git_roots_lock = Mutex.new
|
|
17
|
+
|
|
8
18
|
def self.checkout_vcs(root, screenshot_path, checkout_path)
|
|
9
19
|
root_path = root.to_s
|
|
10
|
-
git_root
|
|
11
|
-
return false unless
|
|
20
|
+
git_root = git_root_for(root_path)
|
|
21
|
+
return false unless git_root
|
|
12
22
|
|
|
13
|
-
git_root = git_root.chomp
|
|
14
23
|
vcs_file_path = Pathname.new(screenshot_path).expand_path.relative_path_from(Pathname.new(git_root)).to_s
|
|
15
24
|
|
|
16
|
-
if
|
|
25
|
+
if SnapDiff.config.use_lfs
|
|
17
26
|
tmp_path = "#{checkout_path}.tmp"
|
|
18
|
-
success = system("git", "-C", root_path, "show", "HEAD:#{vcs_file_path}", out: tmp_path, err: File::NULL)
|
|
27
|
+
success = system(GIT_ENV, "git", "-C", root_path, "show", "HEAD:#{vcs_file_path}", out: tmp_path, err: File::NULL)
|
|
19
28
|
if success
|
|
20
|
-
system("git", "-C", root_path, "lfs", "smudge", in: tmp_path, out: checkout_path.to_s, err: File::NULL)
|
|
29
|
+
system(GIT_ENV, "git", "-C", root_path, "lfs", "smudge", in: tmp_path, out: checkout_path.to_s, err: File::NULL)
|
|
21
30
|
end
|
|
22
31
|
File.delete(tmp_path) if File.exist?(tmp_path)
|
|
23
32
|
else
|
|
24
|
-
success = system("git", "-C", root_path, "show", "HEAD:#{vcs_file_path}", out: checkout_path.to_s, err: File::NULL)
|
|
33
|
+
success = system(GIT_ENV, "git", "-C", root_path, "show", "HEAD:#{vcs_file_path}", out: checkout_path.to_s, err: File::NULL)
|
|
25
34
|
end
|
|
26
35
|
|
|
27
36
|
unless success
|
|
@@ -31,5 +40,29 @@ module SnapDiff
|
|
|
31
40
|
|
|
32
41
|
true
|
|
33
42
|
end
|
|
43
|
+
|
|
44
|
+
# `git rev-parse --show-toplevel` costs a process spawn (~6ms) and used to
|
|
45
|
+
# run once per screenshot -- 200 screenshots, 200 spawns, all answering the
|
|
46
|
+
# same question. The repository a directory belongs to does not change
|
|
47
|
+
# while the suite runs, so remember it per directory. `false` (not a repo)
|
|
48
|
+
# is remembered too: that is the every-assertion answer for anyone whose
|
|
49
|
+
# screenshots live outside a git checkout.
|
|
50
|
+
#
|
|
51
|
+
# Synchronized because the lookup itself is what must not be duplicated:
|
|
52
|
+
# MRI releases the GVL for the whole of `Open3.capture3`, so eight threads
|
|
53
|
+
# asking about one root all miss `key?` before any of them writes -- eight
|
|
54
|
+
# spawns, the exact cost this cache exists to remove. Threads are the
|
|
55
|
+
# default parallel mode on JRuby, which has no GVL to make the Hash write
|
|
56
|
+
# safe either. Holding the lock across the spawn is deliberate: callers
|
|
57
|
+
# almost always share one root, so the other threads wait once and then
|
|
58
|
+
# read the cache, which is the outcome we want.
|
|
59
|
+
def self.git_root_for(root_path)
|
|
60
|
+
@git_roots_lock.synchronize do
|
|
61
|
+
next @git_roots[root_path] if @git_roots.key?(root_path)
|
|
62
|
+
|
|
63
|
+
git_root, _, status = Open3.capture3(GIT_ENV, "git", "-C", root_path, "rev-parse", "--show-toplevel")
|
|
64
|
+
@git_roots[root_path] = status.success? && git_root.chomp
|
|
65
|
+
end
|
|
66
|
+
end
|
|
34
67
|
end
|
|
35
68
|
end
|
data/lib/snap_diff/version.rb
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Bundler.require entry point for `gem "snap_diff-capybara"`: Bundler
|
|
4
|
+
# requires the gem's own name, and its dash->slash fallback ("snap_diff/
|
|
5
|
+
# capybara") misses too, so without this file a Rails user gets a silent
|
|
6
|
+
# no-op and a confusing NameError later. The sibling
|
|
7
|
+
# capybara-screenshot-diff.rb is the same door under the v1 gem name and
|
|
8
|
+
# forwards here.
|
|
9
|
+
#
|
|
10
|
+
# Everything below therefore loads for EVERY consumer, RSpec and Cucumber
|
|
11
|
+
# users included, so nothing outside the gem's declared runtime dependencies
|
|
12
|
+
# may be hard-required. minitest is not one of them -- the gemspec declares
|
|
13
|
+
# capybara only -- and requiring it here killed an RSpec-only bundle at boot
|
|
14
|
+
# with `cannot load such file -- minitest`, from a gem that ships a
|
|
15
|
+
# first-class RSpec integration.
|
|
16
|
+
#
|
|
17
|
+
# So: load the gem, then feature-detect minitest. Present is the documented
|
|
18
|
+
# zero-require Rails path and still activates the assertions. Absent gets a
|
|
19
|
+
# line saying so -- a gem that loads and then does nothing, silently, is its
|
|
20
|
+
# own bug report.
|
|
21
|
+
# This IS the canonical door, and it loads the v1 umbrella below -- so claim
|
|
22
|
+
# the process before that require, or every canonical user is told to
|
|
23
|
+
# migrate off an API they never touched.
|
|
24
|
+
require "snap_diff/deprecation"
|
|
25
|
+
SnapDiff::Deprecation.canonical_entry_point!
|
|
26
|
+
|
|
27
|
+
require "capybara_screenshot_diff"
|
|
28
|
+
|
|
29
|
+
begin
|
|
30
|
+
require "minitest"
|
|
31
|
+
rescue LoadError
|
|
32
|
+
warn "[snap_diff] minitest is not in this bundle, so `Bundler.require` activated no test-framework " \
|
|
33
|
+
"integration. Require the one you use -- `require \"snap_diff/integrations/rspec\"` or " \
|
|
34
|
+
"`require \"snap_diff/integrations/cucumber\"` -- and set `require: false` on the gem in your " \
|
|
35
|
+
"Gemfile to silence this. See docs/framework-setup.md."
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
require "capybara_screenshot_diff/minitest" if defined?(::Minitest)
|
data/lib/snap_diff.rb
CHANGED
|
@@ -6,8 +6,14 @@
|
|
|
6
6
|
# undetectable. Refuse that setup at the entry point. Local dev from
|
|
7
7
|
# source loads neither spec, so the guard fires only when both are
|
|
8
8
|
# genuinely installed as gems.
|
|
9
|
+
#
|
|
10
|
+
# snap_diff/errors is pulled in first (two dependency-free files) so
|
|
11
|
+
# DualInstallError can sit under SnapDiff::Error like every other error the
|
|
12
|
+
# gem raises -- docs/snapdiff.md promises Error is the catch-all.
|
|
13
|
+
require "snap_diff/errors"
|
|
14
|
+
|
|
9
15
|
module SnapDiff
|
|
10
|
-
DualInstallError = Class.new(
|
|
16
|
+
DualInstallError = Class.new(Error)
|
|
11
17
|
|
|
12
18
|
# @api private
|
|
13
19
|
def self.assert_single_gem!(loaded_specs = Gem.loaded_specs)
|
|
@@ -21,59 +27,54 @@ module SnapDiff
|
|
|
21
27
|
end
|
|
22
28
|
SnapDiff.assert_single_gem!
|
|
23
29
|
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
# requires
|
|
28
|
-
#
|
|
29
|
-
#
|
|
30
|
-
# acyclic -- it never requires "capybara_screenshot_diff" back, unlike the
|
|
31
|
-
# old autoload-based wiring this replaced. The forwarder path (not
|
|
32
|
-
# "snap_diff/comparison" directly) is deliberate: it installs
|
|
33
|
-
# snap_diff/legacy_shims, so the old Capybara::Screenshot::Diff constants
|
|
34
|
-
# stay resolvable (now with deprecation warnings) even in processes that
|
|
35
|
-
# only ever require "snap_diff".
|
|
30
|
+
# This lean entry must never load the umbrella "capybara_screenshot_diff"
|
|
31
|
+
# -- snap_diff_test.rb's "bare require never loads the umbrella" guard
|
|
32
|
+
# enforces it -- so nothing required below may reach back here. None of
|
|
33
|
+
# these requires reaches into lib/capybara* at all, so the canonical entry
|
|
34
|
+
# point is exactly what 3.0 keeps.
|
|
35
|
+
#
|
|
36
36
|
# "capybara/dsl" is needed directly (not just transitively) so
|
|
37
37
|
# `Capybara.default_max_wait_time` in Config#default_options resolves even
|
|
38
38
|
# when "snap_diff" is required standalone (SnapDiffTest's
|
|
39
39
|
# "standalone-loadable in a fresh process" regression test).
|
|
40
|
+
#
|
|
41
|
+
# snap_diff/legacy_shims is deliberate and is the ONE line here that 3.0
|
|
42
|
+
# drops: it carries the whole v1 surface (const_missing forwarders, the old
|
|
43
|
+
# mattr_accessors, SnapDiff.start), so a process that only ever requires
|
|
44
|
+
# "snap_diff" still resolves the old Capybara::Screenshot::Diff names --
|
|
45
|
+
# with deprecation warnings -- exactly as it did when this file reached
|
|
46
|
+
# through the capybara/screenshot/diff/* forwarders to get them.
|
|
40
47
|
require "capybara/dsl"
|
|
41
|
-
require "capybara/screenshot/diff/config_legacy"
|
|
42
|
-
require "capybara/screenshot/diff/image_compare"
|
|
43
|
-
require "snap_diff/errors"
|
|
44
48
|
require "snap_diff/config"
|
|
49
|
+
require "snap_diff/comparison"
|
|
50
|
+
require "snap_diff/legacy_shims"
|
|
51
|
+
require "snap_diff/version"
|
|
52
|
+
# SnapDiff.session/.reset/.pending_screenshots_message are part of the
|
|
53
|
+
# documented core surface (docs/snapdiff.md object map lists them with no
|
|
54
|
+
# extra require), so the entry point owns them rather than leaving them to
|
|
55
|
+
# whichever integration happens to be loaded.
|
|
56
|
+
require "snap_diff/screenshot_assertion"
|
|
45
57
|
|
|
46
|
-
#
|
|
47
|
-
#
|
|
48
|
-
#
|
|
49
|
-
# ({SnapDiff::Comparison}, {SnapDiff::ComparisonResult}); since step 6 the
|
|
50
|
-
# old +Capybara::Screenshot::Diff+ constants are same-object const_missing
|
|
51
|
-
# shims (snap_diff/legacy_shims) that emit a deprecation warning once per
|
|
52
|
-
# constant per process. See ADR-004 for the full migration plan.
|
|
58
|
+
# The canonical namespace for the gem. The old
|
|
59
|
+
# +Capybara::Screenshot::Diff+ constants are same-object const_missing shims
|
|
60
|
+
# (snap_diff/legacy_shims) that warn once per constant per process.
|
|
53
61
|
module SnapDiff
|
|
54
|
-
# Compare two images on disk with the configured defaults. Canonical home
|
|
55
|
-
#
|
|
56
|
-
# here rather than the other way round.
|
|
62
|
+
# Compare two images on disk with the configured defaults. Canonical home;
|
|
63
|
+
# +Capybara::Screenshot::Diff.compare+ forwards here.
|
|
57
64
|
#
|
|
58
65
|
# Note the argument order swap: callers pass baseline first (reading
|
|
59
66
|
# "compare baseline against current"), Comparison takes current first.
|
|
60
67
|
def self.compare(baseline_path, current_path, **options)
|
|
68
|
+
# BEFORE the merge, which is the last moment `driver:` still means "the
|
|
69
|
+
# caller asked for a backend" rather than "config.default_options
|
|
70
|
+
# carries the key for everyone".
|
|
71
|
+
Removal.warn_once(:driver_setting, Removal::DRIVER_REMOVED) if options.key?(:driver)
|
|
61
72
|
Comparison.new(current_path, baseline_path, config.default_options.merge(options))
|
|
62
73
|
end
|
|
63
74
|
|
|
64
|
-
# v1-
|
|
65
|
-
# (
|
|
66
|
-
#
|
|
67
|
-
# step 7b, this is where that yield actually happens; Diff.configure
|
|
68
|
-
# forwards here. Both names stay identical in call shape.
|
|
69
|
-
#
|
|
70
|
-
# SnapDiff.start do |screenshot, diff|
|
|
71
|
-
# screenshot.window_size = [1280, 1024]
|
|
72
|
-
# diff.tolerance = 0.0005
|
|
73
|
-
# end
|
|
74
|
-
def self.start
|
|
75
|
-
yield Capybara::Screenshot, Capybara::Screenshot::Diff
|
|
76
|
-
end
|
|
75
|
+
# SnapDiff.start -- the v1-shaped two-holder config block -- is defined in
|
|
76
|
+
# snap_diff/legacy_shims (required above), because the holders it yields
|
|
77
|
+
# are the v1 surface and it cannot outlive them.
|
|
77
78
|
|
|
78
79
|
# Forward-looking configuration: yields the single consolidated
|
|
79
80
|
# {SnapDiff::Config} object instead of the two old holders. Same
|