capybara-screenshot-diff 1.9.0 → 1.9.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 39580aa58be36c5c95baa57516c499050b5806ac156e567cc31e5ce5b3a2c92c
4
- data.tar.gz: 4a529c3e70bd39dbfb7eb10c78d15e24a66e47179445cc2f42ffd63305b3491f
3
+ metadata.gz: 87328c5152c18c006da12b39c30f0389ac0f7b71174530473681ac98e035b884
4
+ data.tar.gz: ec54883ea6de4b75fa5be917fee79e5098a18cd1c5ea3ebbfceb567661e148d3
5
5
  SHA512:
6
- metadata.gz: 3ed30c48e2e2b3e9a4e49ab267ace2eadf4e3c7730ca628f9a0e58bf5f03f7c5d303c52744a1ffd554a3a4498101d4b6615f59d971ad8403a446c40bbac1582f
7
- data.tar.gz: 9a2bd4035a02d2d38840befbf982adad15f45f0ce529dfbc1c8ad13ac047957e6eeee2b49d8bd77455443378f66a9113b584e9b714d0efc93f5639c0aad65788
6
+ metadata.gz: 7e5f89855ddaf008feab8705c6e13f495b654a7dd382fe209ac5e5917fca00553765dfcf411b90cd83aecd931fec6055502dcf3cf7ee0d69c21e8edaa891cbdc
7
+ data.tar.gz: 9f402d5ff1ed20d02c0e6fd3d7d025fc7e82287e7d159a6fb892c21cf0f8bf6db74257ba22560631ef179f191cce2de78f099b4a47b637a9d6f91eaededa7ddd
@@ -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", ">= 6.1", "< 8"
26
+ spec.add_runtime_dependency "actionpack", ">= 6.1", "< 9"
27
27
  spec.add_runtime_dependency "capybara", ">= 2", "< 4"
28
28
  end
@@ -30,13 +30,13 @@ module Capybara
30
30
 
31
31
  IMAGE_WAIT_SCRIPT = <<~JS
32
32
  function pending_image() {
33
- var images = document.images;
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,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "chunky_png"
4
3
  require "capybara/screenshot/diff/difference"
5
4
 
6
5
  module Capybara
@@ -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, :screenshot_path, :base_screenshot_path, :screenshot_format
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
- @screenshot_path = Screenshot.screenshot_area_abs / Pathname.new(screenshot_full_name).sub_ext(".#{screenshot_format}")
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
- # Load base screenshot from VCS
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 && !base_screenshot_path.exist?
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, driver_options, screenshot_path)
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 base_screenshot_path.exist?
48
+ return unless @snapshot.base_path.exist?
62
49
 
63
50
  # Add comparison job in the queue
64
- [screenshot_full_name, ImageCompare.new(screenshot_path, base_screenshot_path, driver_options)]
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 checkout_base_screenshot
74
- Vcs.checkout_vcs(screenshot_path, base_screenshot_path)
75
- end
76
-
77
- def create_output_directory_for(screenshot_path)
78
- screenshot_path.dirname.mkpath
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, driver_options, screenshot_path)
85
- screenshoter = build_screenshoter_for(capture_options, driver_options)
86
- screenshoter.take_comparison_screenshot(screenshot_path)
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, :comparison_options, :driver
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(screenshot_path)
46
- capture_screenshot_at(screenshot_path)
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(screenshot_path)
127
- new_screenshot_path = Screenshoter.gen_next_attempt_path(screenshot_path, 0)
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
- def move_screenshot_to(new_screenshot_path, screenshot_path)
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 = nil)
20
- @stability_time_limit, @wait = capture_options.fetch_values(:stability_time_limit, :wait)
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 || Diff.default_options
26
+ @comparison_options = comparison_options
27
27
 
28
28
  driver = Diff::Drivers.for(@comparison_options)
29
- @screenshoter = Diff.screenshoter.new(capture_options.except(*STABILITY_OPTIONS), driver)
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 screenshot_path [String, Pathname] The path where the screenshot will be saved.
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(screenshot_path)
42
- new_screenshot_path = take_stable_screenshot(screenshot_path)
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 new_screenshot_path
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!(screenshot_path)
47
+ annotate_attempts_and_fail!(snapshot)
48
48
  end
49
49
 
50
- FileUtils.mv(new_screenshot_path, screenshot_path, force: true)
51
- Screenshoter.cleanup_attempts_screenshots(screenshot_path)
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(screenshot_path)
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
- Screenshoter.cleanup_attempts_screenshots(screenshot_path)
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
- attempt_path, prev_attempt_path = attempt_next_screenshot(attempt_path, i, screenshot_path)
67
- return attempt_path if attempt_successful?(attempt_path, prev_attempt_path)
68
- return nil if timeout?(deadline_at)
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?(attempt_path, prev_attempt_path)
75
- return false unless prev_attempt_path
76
- build_comparison_for(attempt_path, prev_attempt_path).quick_equal?
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(prev_attempt_path, i, screenshot_path)
82
- new_attempt_path = Screenshoter.gen_next_attempt_path(screenshot_path, i)
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 build_comparison_for(attempt_path, previous_attempt_path)
92
- ImageCompare.new(attempt_path, previous_attempt_path, @comparison_options)
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!(screenshot_path)
97
- screenshot_attempts = Screenshoter.attempts_screenshot_paths(screenshot_path)
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("Could not get stable screenshot within #{wait}s:\n#{screenshot_attempts.join("\n")}")
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(*)
@@ -146,7 +146,7 @@ module Capybara
146
146
 
147
147
  # Asserts that an image has not changed compared to its baseline.
148
148
  #
149
- # @param caller [Array] The caller context, used for error reporting.
149
+ # @param caller [Array(String)] The caller context, used for error reporting.
150
150
  # @param name [String] The name of the screenshot being verified.
151
151
  # @param comparison [Object] The comparison object containing the result and details of the comparison.
152
152
  # @return [String, nil] Returns an error message if the screenshot differs from the baseline, otherwise nil.
@@ -163,7 +163,7 @@ module Capybara
163
163
 
164
164
  return unless result
165
165
 
166
- "Screenshot does not match for '#{name}' #{comparison.error_message}\n#{caller}"
166
+ "Screenshot does not match for '#{name}' #{comparison.error_message}\n#{caller.join(", ")}"
167
167
  end
168
168
 
169
169
  private
@@ -6,21 +6,35 @@ module Capybara
6
6
  module Screenshot
7
7
  module Diff
8
8
  module Vcs
9
- SILENCE_ERRORS = Os::ON_WINDOWS ? "2>nul" : "2>/dev/null"
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.restore_git_revision(screenshot_path, checkout_path)
12
- vcs_file_path = screenshot_path.relative_path_from(Screenshot.root)
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
- if Screenshot.use_lfs
17
- `#{show_command} > #{checkout_path}.tmp #{SILENCE_ERRORS}`
18
- if $CHILD_STATUS == 0
19
- `git lfs smudge < #{checkout_path}.tmp > #{redirect_target}`
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
@@ -3,7 +3,7 @@
3
3
  module Capybara
4
4
  module Screenshot
5
5
  module Diff
6
- VERSION = "1.9.0"
6
+ VERSION = "1.9.2"
7
7
  end
8
8
  end
9
9
  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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "capybara_screenshot_diff"
4
+ require "capybara/screenshot/diff/test_methods"
4
5
 
5
6
  module CapybaraScreenshotDiff
6
7
  module DSL
@@ -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,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: capybara-screenshot-diff
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.9.0
4
+ version: 1.9.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Uwe Kubosch
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-30 00:00:00.000000000 Z
11
+ date: 2024-11-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack
@@ -19,7 +19,7 @@ dependencies:
19
19
  version: '6.1'
20
20
  - - "<"
21
21
  - !ruby/object:Gem::Version
22
- version: '8'
22
+ version: '9'
23
23
  type: :runtime
24
24
  prerelease: false
25
25
  version_requirements: !ruby/object:Gem::Requirement
@@ -29,7 +29,7 @@ dependencies:
29
29
  version: '6.1'
30
30
  - - "<"
31
31
  - !ruby/object:Gem::Version
32
- version: '8'
32
+ version: '9'
33
33
  - !ruby/object:Gem::Dependency
34
34
  name: capybara
35
35
  requirement: !ruby/object:Gem::Requirement
@@ -84,10 +84,13 @@ files:
84
84
  - lib/capybara/screenshot/diff/vcs.rb
85
85
  - lib/capybara/screenshot/diff/version.rb
86
86
  - lib/capybara_screenshot_diff.rb
87
+ - lib/capybara_screenshot_diff/attempts_reporter.rb
87
88
  - lib/capybara_screenshot_diff/cucumber.rb
88
89
  - lib/capybara_screenshot_diff/dsl.rb
89
90
  - lib/capybara_screenshot_diff/minitest.rb
90
91
  - lib/capybara_screenshot_diff/rspec.rb
92
+ - lib/capybara_screenshot_diff/snap.rb
93
+ - lib/capybara_screenshot_diff/snap_manager.rb
91
94
  homepage: https://github.com/donv/capybara-screenshot-diff
92
95
  licenses:
93
96
  - MIT
@@ -108,7 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
108
111
  - !ruby/object:Gem::Version
109
112
  version: '0'
110
113
  requirements: []
111
- rubygems_version: 3.5.11
114
+ rubygems_version: 3.5.22
112
115
  signing_key:
113
116
  specification_version: 4
114
117
  summary: Track your GUI changes with diff assertions