capybara-screenshot-diff 1.8.3 → 1.9.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -11
  3. data/capybara-screenshot-diff.gemspec +2 -3
  4. data/gems.rb +4 -4
  5. data/lib/capybara/screenshot/diff/area_calculator.rb +56 -0
  6. data/lib/capybara/screenshot/diff/browser_helpers.rb +5 -5
  7. data/lib/capybara/screenshot/diff/comparison.rb +6 -0
  8. data/lib/capybara/screenshot/diff/cucumber.rb +1 -9
  9. data/lib/capybara/screenshot/diff/difference.rb +8 -4
  10. data/lib/capybara/screenshot/diff/drivers/base_driver.rb +0 -5
  11. data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +10 -5
  12. data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +14 -3
  13. data/lib/capybara/screenshot/diff/drivers.rb +1 -1
  14. data/lib/capybara/screenshot/diff/image_compare.rb +80 -114
  15. data/lib/capybara/screenshot/diff/region.rb +28 -7
  16. data/lib/capybara/screenshot/diff/reporters/default.rb +117 -0
  17. data/lib/capybara/screenshot/diff/screenshot_matcher.rb +36 -74
  18. data/lib/capybara/screenshot/diff/screenshoter.rb +46 -54
  19. data/lib/capybara/screenshot/diff/stable_screenshoter.rb +65 -63
  20. data/lib/capybara/screenshot/diff/test_methods.rb +76 -9
  21. data/lib/capybara/screenshot/diff/{drivers/utils.rb → utils.rb} +0 -7
  22. data/lib/capybara/screenshot/diff/vcs.rb +25 -23
  23. data/lib/capybara/screenshot/diff/version.rb +1 -1
  24. data/lib/capybara/screenshot/diff.rb +1 -111
  25. data/lib/capybara-screenshot-diff.rb +1 -1
  26. data/lib/capybara_screenshot_diff/attempts_reporter.rb +49 -0
  27. data/lib/capybara_screenshot_diff/cucumber.rb +12 -0
  28. data/lib/capybara_screenshot_diff/dsl.rb +11 -0
  29. data/lib/capybara_screenshot_diff/minitest.rb +45 -0
  30. data/lib/capybara_screenshot_diff/rspec.rb +32 -0
  31. data/lib/capybara_screenshot_diff/snap.rb +55 -0
  32. data/lib/capybara_screenshot_diff/snap_manager.rb +76 -0
  33. data/lib/capybara_screenshot_diff.rb +86 -0
  34. metadata +20 -39
  35. data/lib/capybara/screenshot/diff/stabilization.rb +0 -0
  36. data/sig/capybara/screenshot/diff/diff.rbs +0 -28
  37. data/sig/capybara/screenshot/diff/difference.rbs +0 -33
  38. data/sig/capybara/screenshot/diff/drivers/base_driver.rbs +0 -63
  39. data/sig/capybara/screenshot/diff/drivers/browser_helpers.rbs +0 -36
  40. data/sig/capybara/screenshot/diff/drivers/chunky_png_driver.rbs +0 -89
  41. data/sig/capybara/screenshot/diff/drivers/utils.rbs +0 -13
  42. data/sig/capybara/screenshot/diff/drivers/vips_driver.rbs +0 -25
  43. data/sig/capybara/screenshot/diff/image_compare.rbs +0 -93
  44. data/sig/capybara/screenshot/diff/os.rbs +0 -11
  45. data/sig/capybara/screenshot/diff/region.rbs +0 -43
  46. data/sig/capybara/screenshot/diff/screenshot_matcher.rbs +0 -60
  47. data/sig/capybara/screenshot/diff/screenshoter.rbs +0 -48
  48. data/sig/capybara/screenshot/diff/stable_screenshoter.rbs +0 -29
  49. data/sig/capybara/screenshot/diff/test_methods.rbs +0 -39
  50. data/sig/capybara/screenshot/diff/vcs.rbs +0 -17
@@ -15,11 +15,20 @@ require_relative "region"
15
15
 
16
16
  require_relative "screenshot_matcher"
17
17
 
18
- # Add the `screenshot` method to ActionDispatch::IntegrationTest
18
+ # == Capybara::Screenshot::Diff::TestMethods
19
+ #
20
+ # This module provides methods for capturing screenshots and verifying them against
21
+ # baseline images to detect visual changes. It's designed to be included in test
22
+ # classes to add visual regression testing capabilities.
23
+
19
24
  module Capybara
20
25
  module Screenshot
21
26
  module Diff
22
27
  module TestMethods
28
+ # @!attribute [rw] test_screenshots
29
+ # @return [Array(Array(Array(String), String, ImageCompare | Minitest::Mock))] An array where each element is an array containing the caller context,
30
+ # the name of the screenshot, and the comparison object. This attribute stores information about each screenshot
31
+ # scheduled for comparison to ensure they do not show any unintended differences.
23
32
  def initialize(*)
24
33
  super
25
34
  @screenshot_counter = nil
@@ -29,6 +38,29 @@ module Capybara
29
38
  @test_screenshots = []
30
39
  end
31
40
 
41
+ # Verifies that all scheduled screenshots do not show any unintended differences.
42
+ #
43
+ # @param screenshots [Array(Array(Array(String), String, ImageCompare))] The list of match screenshots jobs. Defaults to all screenshots taken during the test.
44
+ # @return [Array, nil] Returns an array of error messages if there are screenshot differences, otherwise nil.
45
+ # @note This method is typically called at the end of a test to assert all screenshots are as expected.
46
+ def verify_screenshots!(screenshots = @test_screenshots)
47
+ return unless ::Capybara::Screenshot.active? && ::Capybara::Screenshot::Diff.fail_on_difference
48
+
49
+ test_screenshot_errors = screenshots.map do |caller, name, compare|
50
+ assert_image_not_changed(caller, name, compare)
51
+ end
52
+
53
+ test_screenshot_errors.compact!
54
+
55
+ test_screenshot_errors.presence
56
+ ensure
57
+ screenshots.clear
58
+ end
59
+
60
+ # Builds the full name for a screenshot, incorporating counters and group names for uniqueness.
61
+ #
62
+ # @param name [String] The base name for the screenshot.
63
+ # @return [String] The full, unique name for the screenshot.
32
64
  def build_full_name(name)
33
65
  if @screenshot_counter
34
66
  name = format("%02i_#{name}", @screenshot_counter)
@@ -38,6 +70,9 @@ module Capybara
38
70
  File.join(*group_parts.push(name.to_s))
39
71
  end
40
72
 
73
+ # Determines the directory path for saving screenshots.
74
+ #
75
+ # @return [String] The full path to the directory where screenshots are saved.
41
76
  def screenshot_dir
42
77
  File.join(*([Screenshot.screenshot_area] + group_parts))
43
78
  end
@@ -54,6 +89,13 @@ module Capybara
54
89
  FileUtils.rm_rf screenshot_dir
55
90
  end
56
91
 
92
+ # Schedules a screenshot comparison job for later execution.
93
+ #
94
+ # This method adds a job to the queue of screenshots to be matched. It's used when `Capybara::Screenshot::Diff.delayed`
95
+ # is set to true, allowing for batch processing of screenshot comparisons at a later point, typically at the end of a test.
96
+ #
97
+ # @param job [Array(Array(String), String, ImageCompare)] The job to be scheduled, consisting of the caller context, screenshot name, and comparison object.
98
+ # @return [Boolean] Always returns true, indicating the job was successfully scheduled.
57
99
  def schedule_match_job(job)
58
100
  (@test_screenshots ||= []) << job
59
101
  true
@@ -66,28 +108,49 @@ module Capybara
66
108
  parts
67
109
  end
68
110
 
111
+ # Takes a screenshot and optionally compares it against a baseline image.
112
+ #
113
+ # @param name [String] The name of the screenshot, used to generate the filename.
114
+ # @param skip_stack_frames [Integer] The number of stack frames to skip when reporting errors, for cleaner error messages.
115
+ # @param options [Hash] Additional options for taking the screenshot, such as custom dimensions or selectors.
116
+ # @return [Boolean] Returns true if the screenshot was successfully captured and matches the baseline, false otherwise.
117
+ # @raise [CapybaraScreenshotDiff::ExpectationNotMet] If the screenshot does not match the baseline image and fail_if_new is set to true.
118
+ # @example Capture a full-page screenshot named 'login_page'
119
+ # screenshot('login_page', skip_stack_frames: 1, full: true)
69
120
  def screenshot(name, skip_stack_frames: 0, **options)
70
121
  return false unless Screenshot.active?
71
122
 
72
123
  screenshot_full_name = build_full_name(name)
73
124
  job = build_screenshot_matches_job(screenshot_full_name, options)
74
125
 
75
- return false unless job
126
+ unless job
127
+ if Screenshot::Diff.fail_if_new
128
+ raise_error(<<-ERROR.strip_heredoc, caller(2))
129
+ No existing screenshot found for #{screenshot_full_name}!
130
+ To stop seeing this error disable by `Capybara::Screenshot::Diff.fail_if_new=false`
131
+ ERROR
132
+ end
133
+
134
+ return false
135
+ end
76
136
 
77
- job.prepend(caller[skip_stack_frames])
137
+ job.prepend(caller(skip_stack_frames))
78
138
 
79
139
  if Screenshot::Diff.delayed
80
140
  schedule_match_job(job)
81
141
  else
82
142
  error_msg = assert_image_not_changed(*job)
83
- if error_msg
84
- error = ASSERTION.new(error_msg)
85
- error.set_backtrace(caller(2))
86
- raise error
87
- end
143
+ raise_error(error_msg, caller(2)) if error_msg
88
144
  end
89
145
  end
90
146
 
147
+ # Asserts that an image has not changed compared to its baseline.
148
+ #
149
+ # @param caller [Array(String)] The caller context, used for error reporting.
150
+ # @param name [String] The name of the screenshot being verified.
151
+ # @param comparison [Object] The comparison object containing the result and details of the comparison.
152
+ # @return [String, nil] Returns an error message if the screenshot differs from the baseline, otherwise nil.
153
+ # @note This method is used internally to verify individual screenshots.
91
154
  def assert_image_not_changed(caller, name, comparison)
92
155
  result = comparison.different?
93
156
 
@@ -100,11 +163,15 @@ module Capybara
100
163
 
101
164
  return unless result
102
165
 
103
- "Screenshot does not match for '#{name}' #{comparison.error_message}\n#{caller}"
166
+ "Screenshot does not match for '#{name}' #{comparison.error_message}\n#{caller.join(", ")}"
104
167
  end
105
168
 
106
169
  private
107
170
 
171
+ def raise_error(error_msg, backtrace)
172
+ raise CapybaraScreenshotDiff::ExpectationNotMet.new(error_msg).tap { _1.set_backtrace(backtrace) }
173
+ end
174
+
108
175
  def build_screenshot_matches_job(screenshot_full_name, options)
109
176
  ScreenshotMatcher
110
177
  .new(screenshot_full_name, options)
@@ -36,13 +36,6 @@ module Capybara
36
36
  fail "Wrong adapter #{driver.inspect}. Available adapters: #{AVAILABLE_DRIVERS.inspect}"
37
37
  end
38
38
  end
39
-
40
- def self.detect_test_framework_assert
41
- require "minitest"
42
- ::Minitest::Assertion
43
- rescue
44
- ::RuntimeError
45
- end
46
39
  end
47
40
  end
48
41
  end
@@ -6,39 +6,45 @@ 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
27
- FileUtils.rm_f(checkout_path)
41
+ checkout_path.delete if checkout_path.exist?
28
42
  false
29
43
  else
30
44
  true
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.8.3"
6
+ VERSION = "1.9.2"
7
7
  end
8
8
  end
9
9
  end
@@ -1,113 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "capybara/dsl"
4
- require "capybara/screenshot/diff/version"
5
- require "capybara/screenshot/diff/drivers/utils"
6
- require "capybara/screenshot/diff/image_compare"
7
- require "capybara/screenshot/diff/test_methods"
8
- require "capybara/screenshot/diff/screenshoter"
9
-
10
- module Capybara
11
- module Screenshot
12
- mattr_accessor :add_driver_path
13
- mattr_accessor :add_os_path
14
- mattr_accessor :blur_active_element
15
- mattr_accessor :enabled
16
- mattr_accessor :hide_caret
17
- mattr_reader(:root) { (defined?(Rails.root) && Rails.root) || Pathname(".").expand_path }
18
- mattr_accessor :stability_time_limit
19
- mattr_accessor :window_size
20
- mattr_accessor(:save_path) { "doc/screenshots" }
21
- mattr_accessor(:use_lfs)
22
-
23
- class << self
24
- def root=(path)
25
- @@root = Pathname(path).expand_path
26
- end
27
-
28
- def active?
29
- enabled || (enabled.nil? && Diff.enabled)
30
- end
31
-
32
- def screenshot_area
33
- parts = [Screenshot.save_path]
34
- parts << Capybara.current_driver.to_s if Screenshot.add_driver_path
35
- parts << Os.name if Screenshot.add_os_path
36
- File.join(*parts)
37
- end
38
-
39
- def screenshot_area_abs
40
- root / screenshot_area
41
- end
42
- end
43
-
44
- # Module to track screen shot changes
45
- module Diff
46
- include Capybara::DSL
47
-
48
- mattr_accessor(:delayed) { true }
49
- mattr_accessor :area_size_limit
50
- mattr_accessor :color_distance_limit
51
- mattr_accessor(:enabled) { true }
52
- mattr_accessor :shift_distance_limit
53
- mattr_accessor :skip_area
54
- mattr_accessor(:driver) { :auto }
55
- mattr_accessor :tolerance
56
-
57
- mattr_accessor(:screenshoter) { Screenshoter }
58
-
59
- AVAILABLE_DRIVERS = Utils.detect_available_drivers.freeze
60
- ASSERTION = Utils.detect_test_framework_assert
61
-
62
- def self.default_options
63
- {
64
- area_size_limit: area_size_limit,
65
- color_distance_limit: color_distance_limit,
66
- driver: driver,
67
- shift_distance_limit: shift_distance_limit,
68
- skip_area: skip_area,
69
- stability_time_limit: Screenshot.stability_time_limit,
70
- tolerance: tolerance || ((driver == :vips) ? 0.001 : nil),
71
- wait: Capybara.default_max_wait_time
72
- }
73
- end
74
-
75
- def self.included(klass)
76
- klass.include TestMethods
77
- klass.setup do
78
- BrowserHelpers.resize_to(Screenshot.window_size) if Screenshot.window_size
79
- end
80
-
81
- klass.teardown do
82
- if Screenshot.active? && @test_screenshots.present?
83
- track_failures(@test_screenshots)
84
- @test_screenshots.clear
85
- end
86
- end
87
- end
88
-
89
- private
90
-
91
- EMPTY_LINE = "\n\n"
92
-
93
- def track_failures(screenshots)
94
- test_screenshot_errors = screenshots.map do |caller, name, compare|
95
- assert_image_not_changed(caller, name, compare)
96
- end
97
-
98
- test_screenshot_errors.compact!
99
-
100
- unless test_screenshot_errors.empty?
101
- error = ASSERTION.new(test_screenshot_errors.join(EMPTY_LINE))
102
- error.set_backtrace([])
103
-
104
- if is_a?(::Minitest::Runnable)
105
- failures << error
106
- else
107
- raise error
108
- end
109
- end
110
- end
111
- end
112
- end
113
- end
3
+ require "capybara_screenshot_diff/minitest"
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "capybara/screenshot/diff"
3
+ require "capybara_screenshot_diff/minitest"
@@ -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
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "capybara_screenshot_diff/dsl"
4
+
5
+ World(::CapybaraScreenshotDiff::DSL)
6
+
7
+ Before do
8
+ Capybara::Screenshot::Diff.delayed = false
9
+ if Capybara::Screenshot.active? && Capybara::Screenshot.window_size
10
+ Capybara::Screenshot::BrowserHelpers.resize_to(Capybara::Screenshot.window_size)
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "capybara_screenshot_diff"
4
+ require "capybara/screenshot/diff/test_methods"
5
+
6
+ module CapybaraScreenshotDiff
7
+ module DSL
8
+ include Capybara::DSL
9
+ include Capybara::Screenshot::Diff::TestMethods
10
+ end
11
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "minitest"
4
+ require "capybara_screenshot_diff/dsl"
5
+
6
+ used_deprecated_entrypoint = caller.any? do |path|
7
+ path.include?("capybara-screenshot-diff.rb") || path.include?("capybara/screenshot/diff.rb")
8
+ end
9
+
10
+ if used_deprecated_entrypoint
11
+ warn <<~MSG
12
+ [DEPRECATION] The default activation of `capybara_screenshot_diff/minitest` will be removed.
13
+ Please `require "capybara_screenshot_diff/minitest"` explicitly.
14
+ MSG
15
+ end
16
+
17
+ module CapybaraScreenshotDiff
18
+ module Minitest
19
+ module Assertions
20
+ include ::CapybaraScreenshotDiff::DSL
21
+
22
+ def screenshot(*, **)
23
+ super
24
+ rescue CapybaraScreenshotDiff::ExpectationNotMet => e
25
+ raise ::Minitest::Assertion, e.message
26
+ end
27
+
28
+ alias_method :assert_matches_screenshot, :screenshot
29
+
30
+ def self.included(klass)
31
+ klass.setup do
32
+ if ::Capybara::Screenshot.window_size
33
+ ::Capybara::Screenshot::BrowserHelpers.resize_to(::Capybara::Screenshot.window_size)
34
+ end
35
+ end
36
+
37
+ klass.teardown do
38
+ errors = verify_screenshots!(@test_screenshots)
39
+
40
+ failures << ::Minitest::Assertion.new(errors.join("\n\n")) if errors && !errors.empty?
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+ require "capybara_screenshot_diff/dsl"
5
+
6
+ RSpec::Matchers.define :match_screenshot do |name, **options|
7
+ description { "match a screenshot" }
8
+
9
+ match do |_page|
10
+ screenshot(name, **options)
11
+ true
12
+ end
13
+ end
14
+
15
+ RSpec.configure do |config|
16
+ config.include ::CapybaraScreenshotDiff::DSL, type: :feature
17
+ config.include ::CapybaraScreenshotDiff::DSL, type: :system
18
+
19
+ config.after do
20
+ if self.class.include?(::CapybaraScreenshotDiff::DSL) && ::Capybara::Screenshot.active?
21
+ errors = verify_screenshots!(@test_screenshots)
22
+ # TODO: Use rspec/mock approach to postpone verification
23
+ raise ::CapybaraScreenshotDiff::ExpectationNotMet, errors.join("\n") if errors && !errors.empty?
24
+ end
25
+ end
26
+
27
+ config.before do
28
+ if self.class.include?(::CapybaraScreenshotDiff::DSL) && ::Capybara::Screenshot.window_size
29
+ ::Capybara::Screenshot::BrowserHelpers.resize_to(::Capybara::Screenshot.window_size)
30
+ end
31
+ end
32
+ end
@@ -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