capybara-screenshot-diff 1.9.0 → 1.9.2

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 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