capybara-screenshot-diff 1.9.0 → 1.10.0
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/capybara-screenshot-diff.gemspec +2 -2
- data/gems.rb +7 -5
- data/lib/capybara/screenshot/diff/browser_helpers.rb +4 -4
- data/lib/capybara/screenshot/diff/drivers/base_driver.rb +0 -1
- data/lib/capybara/screenshot/diff/screenshot_matcher.rb +27 -36
- data/lib/capybara/screenshot/diff/screenshoter.rb +7 -34
- data/lib/capybara/screenshot/diff/stable_screenshoter.rb +34 -54
- data/lib/capybara/screenshot/diff/test_methods.rb +8 -7
- data/lib/capybara/screenshot/diff/vcs.rb +24 -22
- data/lib/capybara/screenshot/diff/version.rb +1 -1
- data/lib/capybara_screenshot_diff/attempts_reporter.rb +49 -0
- data/lib/capybara_screenshot_diff/dsl.rb +1 -0
- data/lib/capybara_screenshot_diff/minitest.rb +9 -5
- data/lib/capybara_screenshot_diff/rspec.rb +1 -0
- data/lib/capybara_screenshot_diff/snap.rb +55 -0
- data/lib/capybara_screenshot_diff/snap_manager.rb +76 -0
- data/lib/capybara_screenshot_diff.rb +3 -2
- metadata +11 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b035794cdcf21a354847ddc368d0eeedade734ee20c42f68552baaab5e10adce
|
4
|
+
data.tar.gz: 52053bf5f927c67478b1b5532e42fbaba493000196d55cad9e7f9229207ff9d2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bf40f14eff9443d32aa8e179f97e33f4c36e4ad16a0fa49a23ebbfd50056b916bf74bdd1f64ac4cacf19b2ed1aaf6d948851879b5c814cae4701d94ee5d68287
|
7
|
+
data.tar.gz: '096e47818809b8f8cbca41b77b0044c0a91aa449e635315f090f74325be46a868ae3ee2958b518a29b59f294e9173c46ce4c4bb901349ffea46a2afc7edad0c8'
|
@@ -12,7 +12,7 @@ Gem::Specification.new do |spec|
|
|
12
12
|
spec.summary = "Track your GUI changes with diff assertions"
|
13
13
|
spec.description = "Save screen shots and track changes with graphical diff"
|
14
14
|
spec.homepage = "https://github.com/donv/capybara-screenshot-diff"
|
15
|
-
spec.required_ruby_version = ">= 3.
|
15
|
+
spec.required_ruby_version = ">= 3.1"
|
16
16
|
spec.license = "MIT"
|
17
17
|
spec.metadata["allowed_push_host"] = "https://rubygems.org/"
|
18
18
|
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
@@ -23,6 +23,6 @@ Gem::Specification.new do |spec|
|
|
23
23
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
24
24
|
spec.require_paths = ["lib"]
|
25
25
|
|
26
|
-
spec.add_runtime_dependency "actionpack", ">=
|
26
|
+
spec.add_runtime_dependency "actionpack", ">= 7.0", "< 9"
|
27
27
|
spec.add_runtime_dependency "capybara", ">= 2", "< 4"
|
28
28
|
end
|
data/gems.rb
CHANGED
@@ -12,11 +12,13 @@ gem "chunky_png", ">= 1.3", require: false
|
|
12
12
|
gem "oily_png", platform: :ruby, git: "https://github.com/wvanbergen/oily_png", ref: "44042006e79efd42ce4b52c1d78a4c70f0b4b1b2"
|
13
13
|
gem "ruby-vips", require: false
|
14
14
|
|
15
|
-
|
16
|
-
gem
|
17
|
-
gem "minitest
|
18
|
-
gem "
|
19
|
-
gem "
|
15
|
+
group :test do
|
16
|
+
gem 'mutex_m' # Needed for RubyMine
|
17
|
+
gem "minitest", require: false
|
18
|
+
gem "minitest-stub-const", require: false
|
19
|
+
gem "simplecov", require: false
|
20
|
+
gem "rspec", require: false
|
21
|
+
end
|
20
22
|
|
21
23
|
# Capybara Server
|
22
24
|
gem "puma", require: false
|
@@ -30,13 +30,13 @@ module Capybara
|
|
30
30
|
|
31
31
|
IMAGE_WAIT_SCRIPT = <<~JS
|
32
32
|
function pending_image() {
|
33
|
-
|
33
|
+
const images = document.images
|
34
34
|
for (var i = 0; i < images.length; i++) {
|
35
|
-
if (!images[i].complete) {
|
36
|
-
return images[i].src
|
35
|
+
if (!images[i].complete && images[i].loading !== "lazy") {
|
36
|
+
return images[i].src
|
37
37
|
}
|
38
38
|
}
|
39
|
-
return false
|
39
|
+
return false
|
40
40
|
}(window)
|
41
41
|
JS
|
42
42
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "capybara_screenshot_diff/snap_manager"
|
3
4
|
require_relative "screenshoter"
|
4
5
|
require_relative "stable_screenshoter"
|
5
6
|
require_relative "browser_helpers"
|
@@ -10,15 +11,14 @@ module Capybara
|
|
10
11
|
module Screenshot
|
11
12
|
module Diff
|
12
13
|
class ScreenshotMatcher
|
13
|
-
attr_reader :screenshot_full_name, :driver_options, :
|
14
|
+
attr_reader :screenshot_full_name, :driver_options, :screenshot_format
|
14
15
|
|
15
16
|
def initialize(screenshot_full_name, options = {})
|
16
17
|
@screenshot_full_name = screenshot_full_name
|
17
18
|
@driver_options = Diff.default_options.merge(options)
|
18
19
|
|
19
20
|
@screenshot_format = @driver_options[:screenshot_format]
|
20
|
-
@
|
21
|
-
@base_screenshot_path = ScreenshotMatcher.base_image_path_from(@screenshot_path)
|
21
|
+
@snapshot = CapybaraScreenshotDiff::SnapManager.snapshot(screenshot_full_name, @screenshot_format)
|
22
22
|
end
|
23
23
|
|
24
24
|
def build_screenshot_matches_job
|
@@ -32,58 +32,49 @@ module Capybara
|
|
32
32
|
# TODO: Move this into screenshot stage, in order to re-evaluate coordinates after page updates
|
33
33
|
# Allow nil or single or multiple areas
|
34
34
|
driver_options[:skip_area] = area_calculator.calculate_skip_area
|
35
|
-
|
36
35
|
driver_options[:driver] = Drivers.for(driver_options[:driver])
|
37
36
|
|
38
|
-
|
39
|
-
create_output_directory_for(screenshot_path) unless screenshot_path.exist?
|
40
|
-
|
41
|
-
checkout_base_screenshot
|
37
|
+
@snapshot.checkout_base_screenshot
|
42
38
|
|
43
39
|
# When fail_if_new is true no need to create screenshot if base screenshot is missing
|
44
|
-
return if Capybara::Screenshot::Diff.fail_if_new &&
|
45
|
-
|
46
|
-
capture_options =
|
47
|
-
# screenshot options
|
48
|
-
capybara_screenshot_options: driver_options[:capybara_screenshot_options],
|
49
|
-
crop: driver_options.delete(:crop),
|
50
|
-
# delivery options
|
51
|
-
screenshot_format: driver_options[:screenshot_format],
|
52
|
-
# stability options
|
53
|
-
stability_time_limit: driver_options.delete(:stability_time_limit),
|
54
|
-
wait: driver_options.delete(:wait)
|
55
|
-
}
|
40
|
+
return if Capybara::Screenshot::Diff.fail_if_new && !@snapshot.base_path.exist?
|
41
|
+
|
42
|
+
capture_options, comparison_options = extract_capture_and_comparison_options!(driver_options)
|
56
43
|
|
57
44
|
# Load new screenshot from Browser
|
58
|
-
take_comparison_screenshot(capture_options,
|
45
|
+
take_comparison_screenshot(capture_options, comparison_options, @snapshot)
|
59
46
|
|
60
47
|
# Pre-computation: No need to compare without base screenshot
|
61
|
-
return unless
|
48
|
+
return unless @snapshot.base_path.exist?
|
62
49
|
|
63
50
|
# Add comparison job in the queue
|
64
|
-
[screenshot_full_name, ImageCompare.new(
|
65
|
-
end
|
66
|
-
|
67
|
-
def self.base_image_path_from(screenshot_path)
|
68
|
-
screenshot_path.sub_ext(".base#{screenshot_path.extname}")
|
51
|
+
[screenshot_full_name, ImageCompare.new(@snapshot.path, @snapshot.base_path, comparison_options)]
|
69
52
|
end
|
70
53
|
|
71
54
|
private
|
72
55
|
|
73
|
-
def
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
56
|
+
def extract_capture_and_comparison_options!(driver_options = {})
|
57
|
+
[
|
58
|
+
{
|
59
|
+
# screenshot options
|
60
|
+
capybara_screenshot_options: driver_options[:capybara_screenshot_options],
|
61
|
+
crop: driver_options.delete(:crop),
|
62
|
+
# delivery options
|
63
|
+
screenshot_format: driver_options[:screenshot_format],
|
64
|
+
# stability options
|
65
|
+
stability_time_limit: driver_options.delete(:stability_time_limit),
|
66
|
+
wait: driver_options.delete(:wait)
|
67
|
+
},
|
68
|
+
driver_options
|
69
|
+
]
|
79
70
|
end
|
80
71
|
|
81
72
|
# Try to get screenshot from browser.
|
82
73
|
# On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts
|
83
74
|
# On reaching `wait` limit then it has been failed. On failing we annotate screenshot attempts to help to debug
|
84
|
-
def take_comparison_screenshot(capture_options,
|
85
|
-
screenshoter = build_screenshoter_for(capture_options,
|
86
|
-
screenshoter.take_comparison_screenshot(
|
75
|
+
def take_comparison_screenshot(capture_options, comparison_options, snapshot = nil)
|
76
|
+
screenshoter = build_screenshoter_for(capture_options, comparison_options)
|
77
|
+
screenshoter.take_comparison_screenshot(snapshot)
|
87
78
|
end
|
88
79
|
|
89
80
|
def build_screenshoter_for(capture_options, comparison_options = {})
|
@@ -6,11 +6,10 @@ require_relative "browser_helpers"
|
|
6
6
|
module Capybara
|
7
7
|
module Screenshot
|
8
8
|
class Screenshoter
|
9
|
-
attr_reader :capture_options, :
|
9
|
+
attr_reader :capture_options, :driver
|
10
10
|
|
11
11
|
def initialize(capture_options, driver)
|
12
12
|
@capture_options = capture_options
|
13
|
-
@comparison_options = comparison_options
|
14
13
|
@driver = driver
|
15
14
|
end
|
16
15
|
|
@@ -22,34 +21,16 @@ module Capybara
|
|
22
21
|
@capture_options[:wait]
|
23
22
|
end
|
24
23
|
|
25
|
-
def screenshot_format
|
26
|
-
@capture_options[:screenshot_format] || "png"
|
27
|
-
end
|
28
|
-
|
29
24
|
def capybara_screenshot_options
|
30
25
|
@capture_options[:capybara_screenshot_options] || {}
|
31
26
|
end
|
32
27
|
|
33
|
-
def self.attempts_screenshot_paths(base_file)
|
34
|
-
extname = Pathname.new(base_file).extname
|
35
|
-
Dir["#{base_file.to_s.chomp(extname)}.attempt_*#{extname}"].sort
|
36
|
-
end
|
37
|
-
|
38
|
-
def self.cleanup_attempts_screenshots(base_file)
|
39
|
-
FileUtils.rm_rf attempts_screenshot_paths(base_file)
|
40
|
-
end
|
41
|
-
|
42
28
|
# Try to get screenshot from browser.
|
43
29
|
# On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts
|
44
30
|
# On reaching `wait` limit then it has been failed. On failing we annotate screenshot attempts to help to debug
|
45
|
-
def take_comparison_screenshot(
|
46
|
-
capture_screenshot_at(
|
47
|
-
|
48
|
-
Screenshoter.cleanup_attempts_screenshots(screenshot_path)
|
49
|
-
end
|
50
|
-
|
51
|
-
def self.gen_next_attempt_path(screenshot_path, iteration)
|
52
|
-
screenshot_path.sub_ext(format(".attempt_%02i#{screenshot_path.extname}", iteration))
|
31
|
+
def take_comparison_screenshot(snapshot)
|
32
|
+
capture_screenshot_at(snapshot)
|
33
|
+
snapshot.cleanup_attempts
|
53
34
|
end
|
54
35
|
|
55
36
|
PNG_EXTENSION = ".png"
|
@@ -123,18 +104,10 @@ module Capybara
|
|
123
104
|
File.unlink(tmpfile) if tmpfile
|
124
105
|
end
|
125
106
|
|
126
|
-
def capture_screenshot_at(
|
127
|
-
|
128
|
-
take_and_process_screenshot(new_screenshot_path, screenshot_path)
|
129
|
-
end
|
130
|
-
|
131
|
-
def take_and_process_screenshot(new_screenshot_path, screenshot_path)
|
132
|
-
take_screenshot(new_screenshot_path)
|
133
|
-
move_screenshot_to(new_screenshot_path, screenshot_path)
|
134
|
-
end
|
107
|
+
def capture_screenshot_at(snapshot)
|
108
|
+
take_screenshot(snapshot.next_attempt_path!)
|
135
109
|
|
136
|
-
|
137
|
-
FileUtils.mv(new_screenshot_path, screenshot_path, force: true)
|
110
|
+
snapshot.commit_last_attempt
|
138
111
|
end
|
139
112
|
|
140
113
|
def resize_if_needed(saved_image)
|
@@ -16,17 +16,17 @@ module Capybara
|
|
16
16
|
# @param capture_options [Hash] The options for capturing screenshots, must include `:stability_time_limit` and `:wait`.
|
17
17
|
# @param comparison_options [Hash, nil] The options for comparing screenshots, defaults to `nil` which uses `Diff.default_options`.
|
18
18
|
# @raise [ArgumentError] If `:wait` or `:stability_time_limit` are not provided, or if `:stability_time_limit` is greater than `:wait`.
|
19
|
-
def initialize(capture_options, comparison_options =
|
20
|
-
@stability_time_limit, @wait = capture_options.fetch_values(
|
19
|
+
def initialize(capture_options, comparison_options = {})
|
20
|
+
@stability_time_limit, @wait = capture_options.fetch_values(*STABILITY_OPTIONS)
|
21
21
|
|
22
22
|
raise ArgumentError, "wait should be provided for stable screenshots" unless wait
|
23
23
|
raise ArgumentError, "stability_time_limit should be provided for stable screenshots" unless stability_time_limit
|
24
24
|
raise ArgumentError, "stability_time_limit (#{stability_time_limit}) should be less or equal than wait (#{wait}) for stable screenshots" unless stability_time_limit <= wait
|
25
25
|
|
26
|
-
@comparison_options = comparison_options
|
26
|
+
@comparison_options = comparison_options
|
27
27
|
|
28
28
|
driver = Diff::Drivers.for(@comparison_options)
|
29
|
-
@screenshoter = Diff.screenshoter.new(capture_options.except(
|
29
|
+
@screenshoter = Diff.screenshoter.new(capture_options.except(:stability_time_limit), driver)
|
30
30
|
end
|
31
31
|
|
32
32
|
# Takes a comparison screenshot ensuring page stability
|
@@ -35,92 +35,72 @@ module Capybara
|
|
35
35
|
# or the `:wait` limit is reached. If unable to achieve a stable state within the time limit, it annotates the attempts
|
36
36
|
# to aid debugging.
|
37
37
|
#
|
38
|
-
# @param
|
38
|
+
# @param snapshot Snap The snapshot details to take a stable screenshot of.
|
39
39
|
# @return [void]
|
40
40
|
# @raise [RuntimeError] If a stable screenshot cannot be obtained within the specified `:wait` time.
|
41
|
-
def take_comparison_screenshot(
|
42
|
-
|
41
|
+
def take_comparison_screenshot(snapshot)
|
42
|
+
result = take_stable_screenshot(snapshot)
|
43
43
|
|
44
44
|
# We failed to get stable browser state! Generate difference between attempts to overview moving parts!
|
45
|
-
unless
|
45
|
+
unless result
|
46
46
|
# FIXME(uwe): Change to store the failure and only report if the test succeeds functionally.
|
47
|
-
annotate_attempts_and_fail!(
|
47
|
+
annotate_attempts_and_fail!(snapshot)
|
48
48
|
end
|
49
49
|
|
50
|
-
|
51
|
-
|
50
|
+
# store success attempt as actual screenshot
|
51
|
+
snapshot.commit_last_attempt
|
52
|
+
|
53
|
+
# cleanup all previous attempts
|
54
|
+
snapshot.cleanup_attempts
|
52
55
|
end
|
53
56
|
|
54
|
-
def take_stable_screenshot(
|
55
|
-
screenshot_path = screenshot_path.is_a?(String) ? Pathname.new(screenshot_path) : screenshot_path
|
57
|
+
def take_stable_screenshot(snapshot)
|
56
58
|
# We try to compare first attempt with checkout version, in order to not run next screenshots
|
57
|
-
attempt_path = nil
|
58
59
|
deadline_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + wait
|
59
60
|
|
60
61
|
# Cleanup all previous attempts for sure
|
61
|
-
|
62
|
+
snapshot.cleanup_attempts
|
62
63
|
|
63
64
|
0.step do |i|
|
64
65
|
# FIXME: it should be wait, and wait should be replaced with stability_time_limit
|
65
|
-
sleep(stability_time_limit) unless i == 0
|
66
|
-
|
67
|
-
|
68
|
-
|
66
|
+
sleep(stability_time_limit) unless i == 0 # test prev_attempt_path is nil
|
67
|
+
|
68
|
+
attempt_next_screenshot(snapshot)
|
69
|
+
|
70
|
+
return true if attempt_successful?(snapshot)
|
71
|
+
return false if timeout?(deadline_at)
|
69
72
|
end
|
70
73
|
end
|
71
74
|
|
72
75
|
private
|
73
76
|
|
74
|
-
def attempt_successful?(
|
75
|
-
return false unless prev_attempt_path
|
76
|
-
|
77
|
+
def attempt_successful?(snapshot)
|
78
|
+
return false unless snapshot.prev_attempt_path
|
79
|
+
|
80
|
+
build_last_attempts_comparison_for(snapshot).quick_equal?
|
77
81
|
rescue ArgumentError
|
78
82
|
false
|
79
83
|
end
|
80
84
|
|
81
|
-
def attempt_next_screenshot(
|
82
|
-
|
83
|
-
@screenshoter.take_screenshot(new_attempt_path)
|
84
|
-
[new_attempt_path, prev_attempt_path]
|
85
|
+
def attempt_next_screenshot(snapshot)
|
86
|
+
@screenshoter.take_screenshot(snapshot.next_attempt_path!)
|
85
87
|
end
|
86
88
|
|
87
89
|
def timeout?(deadline_at)
|
88
90
|
Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline_at
|
89
91
|
end
|
90
92
|
|
91
|
-
def
|
92
|
-
ImageCompare.new(attempt_path,
|
93
|
+
def build_last_attempts_comparison_for(snapshot)
|
94
|
+
ImageCompare.new(snapshot.attempt_path, snapshot.prev_attempt_path, @comparison_options)
|
93
95
|
end
|
94
96
|
|
95
97
|
# TODO: Move to the HistoricalReporter
|
96
|
-
def annotate_attempts_and_fail!(
|
97
|
-
|
98
|
-
|
99
|
-
annotate_stabilization_images(screenshot_attempts)
|
98
|
+
def annotate_attempts_and_fail!(snapshot)
|
99
|
+
require "capybara_screenshot_diff/attempts_reporter"
|
100
|
+
attempts_reporter = CapybaraScreenshotDiff::AttemptsReporter.new(snapshot, @comparison_options, {wait: wait, stability_time_limit: stability_time_limit})
|
100
101
|
|
101
102
|
# TODO: Move fail to the queue after tests passed
|
102
|
-
fail(
|
103
|
-
end
|
104
|
-
|
105
|
-
# TODO: Add tests that we annotate all files except first one
|
106
|
-
def annotate_stabilization_images(attempts_screenshot_paths)
|
107
|
-
previous_file = nil
|
108
|
-
attempts_screenshot_paths.reverse_each do |file_name|
|
109
|
-
if previous_file && File.exist?(previous_file)
|
110
|
-
attempts_comparison = build_comparison_for(file_name, previous_file)
|
111
|
-
|
112
|
-
if attempts_comparison.different?
|
113
|
-
FileUtils.mv(attempts_comparison.reporter.annotated_base_image_path, previous_file, force: true)
|
114
|
-
else
|
115
|
-
warn "[capybara-screenshot-diff] Some attempts was stable, but mistakenly marked as not: " \
|
116
|
-
"#{previous_file} and #{file_name} are equal"
|
117
|
-
end
|
118
|
-
|
119
|
-
FileUtils.rm(attempts_comparison.reporter.annotated_image_path, force: true)
|
120
|
-
end
|
121
|
-
|
122
|
-
previous_file = file_name
|
123
|
-
end
|
103
|
+
fail(attempts_reporter.generate)
|
124
104
|
end
|
125
105
|
end
|
126
106
|
end
|
@@ -26,7 +26,7 @@ module Capybara
|
|
26
26
|
module Diff
|
27
27
|
module TestMethods
|
28
28
|
# @!attribute [rw] test_screenshots
|
29
|
-
# @return [Array(Array(Array(String), String, ImageCompare))] An array where each element is an array containing the caller context,
|
29
|
+
# @return [Array(Array(Array(String), String, ImageCompare | Minitest::Mock))] An array where each element is an array containing the caller context,
|
30
30
|
# the name of the screenshot, and the comparison object. This attribute stores information about each screenshot
|
31
31
|
# scheduled for comparison to ensure they do not show any unintended differences.
|
32
32
|
def initialize(*)
|
@@ -123,9 +123,10 @@ module Capybara
|
|
123
123
|
screenshot_full_name = build_full_name(name)
|
124
124
|
job = build_screenshot_matches_job(screenshot_full_name, options)
|
125
125
|
|
126
|
+
caller = caller(skip_stack_frames + 1).reject { |l| l =~ /gems\/(activesupport|minitest|railties)/ }
|
126
127
|
unless job
|
127
128
|
if Screenshot::Diff.fail_if_new
|
128
|
-
|
129
|
+
_raise_error(<<-ERROR.strip_heredoc, caller)
|
129
130
|
No existing screenshot found for #{screenshot_full_name}!
|
130
131
|
To stop seeing this error disable by `Capybara::Screenshot::Diff.fail_if_new=false`
|
131
132
|
ERROR
|
@@ -134,19 +135,19 @@ module Capybara
|
|
134
135
|
return false
|
135
136
|
end
|
136
137
|
|
137
|
-
job.prepend(caller
|
138
|
+
job.prepend(caller)
|
138
139
|
|
139
140
|
if Screenshot::Diff.delayed
|
140
141
|
schedule_match_job(job)
|
141
142
|
else
|
142
143
|
error_msg = assert_image_not_changed(*job)
|
143
|
-
|
144
|
+
_raise_error(error_msg, caller(2)) if error_msg
|
144
145
|
end
|
145
146
|
end
|
146
147
|
|
147
148
|
# Asserts that an image has not changed compared to its baseline.
|
148
149
|
#
|
149
|
-
# @param caller [Array] The caller context, used for error reporting.
|
150
|
+
# @param caller [Array(String)] The caller context, used for error reporting.
|
150
151
|
# @param name [String] The name of the screenshot being verified.
|
151
152
|
# @param comparison [Object] The comparison object containing the result and details of the comparison.
|
152
153
|
# @return [String, nil] Returns an error message if the screenshot differs from the baseline, otherwise nil.
|
@@ -163,12 +164,12 @@ module Capybara
|
|
163
164
|
|
164
165
|
return unless result
|
165
166
|
|
166
|
-
"Screenshot does not match for '#{name}' #{comparison.error_message}\n#{caller}"
|
167
|
+
"Screenshot does not match for '#{name}' #{comparison.error_message}\n#{caller.join("\n")}"
|
167
168
|
end
|
168
169
|
|
169
170
|
private
|
170
171
|
|
171
|
-
def
|
172
|
+
def _raise_error(error_msg, backtrace)
|
172
173
|
raise CapybaraScreenshotDiff::ExpectationNotMet.new(error_msg).tap { _1.set_backtrace(backtrace) }
|
173
174
|
end
|
174
175
|
|
@@ -6,21 +6,35 @@ module Capybara
|
|
6
6
|
module Screenshot
|
7
7
|
module Diff
|
8
8
|
module Vcs
|
9
|
-
|
9
|
+
def self.checkout_vcs(root, screenshot_path, checkout_path)
|
10
|
+
if svn?(root)
|
11
|
+
restore_svn_revision(screenshot_path, checkout_path)
|
12
|
+
else
|
13
|
+
restore_git_revision(screenshot_path, checkout_path, root: root)
|
14
|
+
end
|
15
|
+
end
|
10
16
|
|
11
|
-
def self.
|
12
|
-
|
17
|
+
def self.svn?(root)
|
18
|
+
(root / ".svn").exist?
|
19
|
+
end
|
20
|
+
|
21
|
+
SILENCE_ERRORS = Os::ON_WINDOWS ? "2>nul" : "2>/dev/null"
|
13
22
|
|
23
|
+
def self.restore_git_revision(screenshot_path, checkout_path = screenshot_path, root:)
|
24
|
+
vcs_file_path = screenshot_path.relative_path_from(root)
|
14
25
|
redirect_target = "#{checkout_path} #{SILENCE_ERRORS}"
|
15
26
|
show_command = "git show HEAD~0:./#{vcs_file_path}"
|
16
|
-
|
17
|
-
|
18
|
-
if
|
19
|
-
|
27
|
+
|
28
|
+
Dir.chdir(root) do
|
29
|
+
if Screenshot.use_lfs
|
30
|
+
system("#{show_command} > #{checkout_path}.tmp #{SILENCE_ERRORS}", exception: !!ENV["DEBUG"])
|
31
|
+
|
32
|
+
`git lfs smudge < #{checkout_path}.tmp > #{redirect_target}` if $CHILD_STATUS == 0
|
33
|
+
|
34
|
+
File.delete "#{checkout_path}.tmp"
|
35
|
+
else
|
36
|
+
system("#{show_command} > #{redirect_target}", exception: !!ENV["DEBUG"])
|
20
37
|
end
|
21
|
-
File.delete "#{checkout_path}.tmp"
|
22
|
-
else
|
23
|
-
`#{show_command} > #{redirect_target}`
|
24
38
|
end
|
25
39
|
|
26
40
|
if $CHILD_STATUS != 0
|
@@ -31,14 +45,6 @@ module Capybara
|
|
31
45
|
end
|
32
46
|
end
|
33
47
|
|
34
|
-
def self.checkout_vcs(screenshot_path, checkout_path)
|
35
|
-
if svn?
|
36
|
-
restore_svn_revision(screenshot_path, checkout_path)
|
37
|
-
else
|
38
|
-
restore_git_revision(screenshot_path, checkout_path)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
48
|
def self.restore_svn_revision(screenshot_path, checkout_path)
|
43
49
|
committed_file_name = screenshot_path + "../.svn/text-base/" + "#{screenshot_path.basename}.svn-base"
|
44
50
|
if committed_file_name.exist?
|
@@ -60,10 +66,6 @@ module Capybara
|
|
60
66
|
|
61
67
|
false
|
62
68
|
end
|
63
|
-
|
64
|
-
def self.svn?
|
65
|
-
(Screenshot.screenshot_area_abs / ".svn").exist?
|
66
|
-
end
|
67
69
|
end
|
68
70
|
end
|
69
71
|
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "capybara/screenshot/diff/image_compare"
|
4
|
+
|
5
|
+
module CapybaraScreenshotDiff
|
6
|
+
class AttemptsReporter
|
7
|
+
def initialize(snapshot, comparison_options, stability_options = {})
|
8
|
+
@snapshot = snapshot
|
9
|
+
@comparison_options = comparison_options
|
10
|
+
@wait = stability_options[:wait]
|
11
|
+
end
|
12
|
+
|
13
|
+
def generate
|
14
|
+
attempts_screenshot_paths = @snapshot.find_attempts_paths
|
15
|
+
|
16
|
+
annotate_attempts(attempts_screenshot_paths)
|
17
|
+
|
18
|
+
"Could not get stable screenshot within #{@wait}s:\n#{attempts_screenshot_paths.join("\n")}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def build_comparison_for(attempt_path, previous_attempt_path)
|
22
|
+
Capybara::Screenshot::Diff::ImageCompare.new(attempt_path, previous_attempt_path, @comparison_options)
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def annotate_attempts(attempts_screenshot_paths)
|
28
|
+
previous_file = nil
|
29
|
+
attempts_screenshot_paths.reverse_each do |file_name|
|
30
|
+
if previous_file && File.exist?(previous_file)
|
31
|
+
attempts_comparison = build_comparison_for(file_name, previous_file)
|
32
|
+
|
33
|
+
if attempts_comparison.different?
|
34
|
+
FileUtils.mv(attempts_comparison.reporter.annotated_base_image_path, previous_file, force: true)
|
35
|
+
else
|
36
|
+
warn "[capybara-screenshot-diff] Some attempts was stable, but mistakenly marked as not: " \
|
37
|
+
"#{previous_file} and #{file_name} are equal"
|
38
|
+
end
|
39
|
+
|
40
|
+
FileUtils.rm(attempts_comparison.reporter.annotated_image_path, force: true)
|
41
|
+
end
|
42
|
+
|
43
|
+
previous_file = file_name
|
44
|
+
end
|
45
|
+
|
46
|
+
previous_file
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -19,10 +19,10 @@ module CapybaraScreenshotDiff
|
|
19
19
|
module Assertions
|
20
20
|
include ::CapybaraScreenshotDiff::DSL
|
21
21
|
|
22
|
-
def screenshot(
|
23
|
-
|
24
|
-
|
25
|
-
|
22
|
+
def screenshot(*args, skip_stack_frames: 0, **opts)
|
23
|
+
assert_nothing_raised do
|
24
|
+
super(*args, skip_stack_frames: skip_stack_frames + 1, **opts)
|
25
|
+
end
|
26
26
|
end
|
27
27
|
|
28
28
|
alias_method :assert_matches_screenshot, :screenshot
|
@@ -37,7 +37,11 @@ module CapybaraScreenshotDiff
|
|
37
37
|
klass.teardown do
|
38
38
|
errors = verify_screenshots!(@test_screenshots)
|
39
39
|
|
40
|
-
|
40
|
+
if errors.present?
|
41
|
+
assertion = ::Minitest::Assertion.new(errors.join("\n\n"))
|
42
|
+
assertion.set_backtrace []
|
43
|
+
failures << assertion
|
44
|
+
end
|
41
45
|
end
|
42
46
|
end
|
43
47
|
end
|
@@ -14,6 +14,7 @@ end
|
|
14
14
|
|
15
15
|
RSpec.configure do |config|
|
16
16
|
config.include ::CapybaraScreenshotDiff::DSL, type: :feature
|
17
|
+
config.include ::CapybaraScreenshotDiff::DSL, type: :system
|
17
18
|
|
18
19
|
config.after do
|
19
20
|
if self.class.include?(::CapybaraScreenshotDiff::DSL) && ::Capybara::Screenshot.active?
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CapybaraScreenshotDiff
|
4
|
+
class Snap
|
5
|
+
attr_reader :full_name, :format, :path, :base_path, :manager, :attempt_path, :prev_attempt_path, :attempts_count
|
6
|
+
|
7
|
+
def initialize(full_name, format, manager: SnapManager.instance)
|
8
|
+
@full_name = full_name
|
9
|
+
@format = format
|
10
|
+
@path = manager.abs_path_for(Pathname.new(@full_name).sub_ext(".#{@format}"))
|
11
|
+
@base_path = @path.sub_ext(".base.#{@format}")
|
12
|
+
@manager = manager
|
13
|
+
@attempts_count = 0
|
14
|
+
end
|
15
|
+
|
16
|
+
def delete!
|
17
|
+
path.delete if path.exist?
|
18
|
+
base_path.delete if base_path.exist?
|
19
|
+
cleanup_attempts
|
20
|
+
end
|
21
|
+
|
22
|
+
def checkout_base_screenshot
|
23
|
+
@manager.checkout_file(path, base_path)
|
24
|
+
end
|
25
|
+
|
26
|
+
def path_for(version = :actual)
|
27
|
+
case version
|
28
|
+
when :base
|
29
|
+
base_path
|
30
|
+
else
|
31
|
+
path
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def next_attempt_path!
|
36
|
+
@prev_attempt_path = @attempt_path
|
37
|
+
@attempt_path = path.sub_ext(sprintf(".attempt_%02i.#{format}", @attempts_count))
|
38
|
+
ensure
|
39
|
+
@attempts_count += 1
|
40
|
+
end
|
41
|
+
|
42
|
+
def commit_last_attempt
|
43
|
+
@manager.move(attempt_path, path)
|
44
|
+
end
|
45
|
+
|
46
|
+
def cleanup_attempts
|
47
|
+
@manager.cleanup_attempts!(self)
|
48
|
+
@attempts_count = 0
|
49
|
+
end
|
50
|
+
|
51
|
+
def find_attempts_paths
|
52
|
+
Dir[@manager.abs_path_for "**/#{full_name}.attempt_*.#{format}"]
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "capybara/screenshot/diff/vcs"
|
4
|
+
require "active_support/core_ext/module/attribute_accessors"
|
5
|
+
|
6
|
+
require "capybara_screenshot_diff/snap"
|
7
|
+
|
8
|
+
module CapybaraScreenshotDiff
|
9
|
+
class SnapManager
|
10
|
+
attr_reader :root
|
11
|
+
|
12
|
+
def initialize(root)
|
13
|
+
@root = Pathname.new(root)
|
14
|
+
end
|
15
|
+
|
16
|
+
def snapshot(screenshot_full_name, screenshot_format = "png")
|
17
|
+
Snap.new(screenshot_full_name, screenshot_format, manager: self)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.snapshot(screenshot_full_name, screenshot_format = "png")
|
21
|
+
instance.snapshot(screenshot_full_name, screenshot_format)
|
22
|
+
end
|
23
|
+
|
24
|
+
def abs_path_for(relative_path)
|
25
|
+
@root / relative_path
|
26
|
+
end
|
27
|
+
|
28
|
+
def checkout_file(path, as_path)
|
29
|
+
create_output_directory_for(as_path) unless as_path.exist?
|
30
|
+
Capybara::Screenshot::Diff::Vcs.checkout_vcs(root, path, as_path)
|
31
|
+
end
|
32
|
+
|
33
|
+
def provision_snap_with(snap, path, version: :actual)
|
34
|
+
managed_path = snap.path_for(version)
|
35
|
+
create_output_directory_for(managed_path) unless managed_path.exist?
|
36
|
+
FileUtils.cp(path, managed_path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def create_output_directory_for(path = nil)
|
40
|
+
path ? path.dirname.mkpath : root.mkpath
|
41
|
+
end
|
42
|
+
|
43
|
+
# TODO: rename to delete!
|
44
|
+
def cleanup!
|
45
|
+
FileUtils.rm_rf root, secure: true
|
46
|
+
end
|
47
|
+
|
48
|
+
def self.cleanup!
|
49
|
+
instance.cleanup!
|
50
|
+
end
|
51
|
+
|
52
|
+
def cleanup_attempts!(snapshot)
|
53
|
+
FileUtils.rm_rf snapshot.find_attempts_paths, secure: true
|
54
|
+
end
|
55
|
+
|
56
|
+
def move(new_screenshot_path, screenshot_path)
|
57
|
+
FileUtils.mv(new_screenshot_path, screenshot_path, force: true)
|
58
|
+
end
|
59
|
+
|
60
|
+
def screenshots
|
61
|
+
root.children.map { |f| f.basename.to_s }
|
62
|
+
end
|
63
|
+
|
64
|
+
def self.screenshots
|
65
|
+
instance.screenshots
|
66
|
+
end
|
67
|
+
|
68
|
+
def self.root
|
69
|
+
instance.root
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.instance
|
73
|
+
Capybara::Screenshot::Diff.manager.new(Capybara::Screenshot.screenshot_area_abs)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -4,9 +4,9 @@ require "capybara/dsl"
|
|
4
4
|
require "capybara/screenshot/diff/version"
|
5
5
|
require "capybara/screenshot/diff/utils"
|
6
6
|
require "capybara/screenshot/diff/image_compare"
|
7
|
+
require "capybara_screenshot_diff/snap_manager"
|
7
8
|
require "capybara/screenshot/diff/test_methods"
|
8
9
|
require "capybara/screenshot/diff/screenshoter"
|
9
|
-
|
10
10
|
require "capybara/screenshot/diff/reporters/default"
|
11
11
|
|
12
12
|
module CapybaraScreenshotDiff
|
@@ -20,7 +20,7 @@ module Capybara
|
|
20
20
|
mattr_accessor :blur_active_element
|
21
21
|
mattr_accessor :enabled
|
22
22
|
mattr_accessor :hide_caret
|
23
|
-
mattr_reader(:root) { (defined?(Rails.root) && Rails.root) || Pathname(".").expand_path }
|
23
|
+
mattr_reader(:root) { (defined?(Rails) && defined?(Rails.root) && Rails.root) || Pathname(".").expand_path }
|
24
24
|
mattr_accessor :stability_time_limit
|
25
25
|
mattr_accessor :window_size
|
26
26
|
mattr_accessor(:save_path) { "doc/screenshots" }
|
@@ -63,6 +63,7 @@ module Capybara
|
|
63
63
|
mattr_accessor :tolerance
|
64
64
|
|
65
65
|
mattr_accessor(:screenshoter) { Screenshoter }
|
66
|
+
mattr_accessor(:manager) { CapybaraScreenshotDiff::SnapManager }
|
66
67
|
|
67
68
|
AVAILABLE_DRIVERS = Utils.detect_available_drivers.freeze
|
68
69
|
|
metadata
CHANGED
@@ -1,14 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: capybara-screenshot-diff
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.10.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Uwe Kubosch
|
8
|
-
autorequire:
|
9
8
|
bindir: exe
|
10
9
|
cert_chain: []
|
11
|
-
date: 2024-
|
10
|
+
date: 2024-12-31 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: actionpack
|
@@ -16,20 +15,20 @@ dependencies:
|
|
16
15
|
requirements:
|
17
16
|
- - ">="
|
18
17
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
18
|
+
version: '7.0'
|
20
19
|
- - "<"
|
21
20
|
- !ruby/object:Gem::Version
|
22
|
-
version: '
|
21
|
+
version: '9'
|
23
22
|
type: :runtime
|
24
23
|
prerelease: false
|
25
24
|
version_requirements: !ruby/object:Gem::Requirement
|
26
25
|
requirements:
|
27
26
|
- - ">="
|
28
27
|
- !ruby/object:Gem::Version
|
29
|
-
version: '
|
28
|
+
version: '7.0'
|
30
29
|
- - "<"
|
31
30
|
- !ruby/object:Gem::Version
|
32
|
-
version: '
|
31
|
+
version: '9'
|
33
32
|
- !ruby/object:Gem::Dependency
|
34
33
|
name: capybara
|
35
34
|
requirement: !ruby/object:Gem::Requirement
|
@@ -84,16 +83,18 @@ files:
|
|
84
83
|
- lib/capybara/screenshot/diff/vcs.rb
|
85
84
|
- lib/capybara/screenshot/diff/version.rb
|
86
85
|
- lib/capybara_screenshot_diff.rb
|
86
|
+
- lib/capybara_screenshot_diff/attempts_reporter.rb
|
87
87
|
- lib/capybara_screenshot_diff/cucumber.rb
|
88
88
|
- lib/capybara_screenshot_diff/dsl.rb
|
89
89
|
- lib/capybara_screenshot_diff/minitest.rb
|
90
90
|
- lib/capybara_screenshot_diff/rspec.rb
|
91
|
+
- lib/capybara_screenshot_diff/snap.rb
|
92
|
+
- lib/capybara_screenshot_diff/snap_manager.rb
|
91
93
|
homepage: https://github.com/donv/capybara-screenshot-diff
|
92
94
|
licenses:
|
93
95
|
- MIT
|
94
96
|
metadata:
|
95
97
|
allowed_push_host: https://rubygems.org/
|
96
|
-
post_install_message:
|
97
98
|
rdoc_options: []
|
98
99
|
require_paths:
|
99
100
|
- lib
|
@@ -101,15 +102,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
101
102
|
requirements:
|
102
103
|
- - ">="
|
103
104
|
- !ruby/object:Gem::Version
|
104
|
-
version: 3.
|
105
|
+
version: '3.1'
|
105
106
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
107
|
requirements:
|
107
108
|
- - ">="
|
108
109
|
- !ruby/object:Gem::Version
|
109
110
|
version: '0'
|
110
111
|
requirements: []
|
111
|
-
rubygems_version: 3.
|
112
|
-
signing_key:
|
112
|
+
rubygems_version: 3.6.2
|
113
113
|
specification_version: 4
|
114
114
|
summary: Track your GUI changes with diff assertions
|
115
115
|
test_files: []
|