capybara-screenshot-diff 1.8.3 → 1.10.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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -11
  3. data/capybara-screenshot-diff.gemspec +3 -4
  4. data/gems.rb +11 -8
  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 +84 -114
  15. data/lib/capybara/screenshot/diff/region.rb +28 -7
  16. data/lib/capybara/screenshot/diff/reporters/default.rb +121 -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 +78 -10
  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 +49 -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 -42
  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,45 +108,71 @@ 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
+ caller = caller(skip_stack_frames + 1).reject { |l| l =~ /gems\/(activesupport|minitest|railties)/ }
127
+ unless job
128
+ if Screenshot::Diff.fail_if_new
129
+ _raise_error(<<-ERROR.strip_heredoc, caller)
130
+ No existing screenshot found for #{screenshot_full_name}!
131
+ To stop seeing this error disable by `Capybara::Screenshot::Diff.fail_if_new=false`
132
+ ERROR
133
+ end
134
+
135
+ return false
136
+ end
76
137
 
77
- job.prepend(caller[skip_stack_frames])
138
+ job.prepend(caller)
78
139
 
79
140
  if Screenshot::Diff.delayed
80
141
  schedule_match_job(job)
81
142
  else
82
143
  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
144
+ _raise_error(error_msg, caller(2)) if error_msg
88
145
  end
89
146
  end
90
147
 
148
+ # Asserts that an image has not changed compared to its baseline.
149
+ #
150
+ # @param caller [Array(String)] The caller context, used for error reporting.
151
+ # @param name [String] The name of the screenshot being verified.
152
+ # @param comparison [Object] The comparison object containing the result and details of the comparison.
153
+ # @return [String, nil] Returns an error message if the screenshot differs from the baseline, otherwise nil.
154
+ # @note This method is used internally to verify individual screenshots.
91
155
  def assert_image_not_changed(caller, name, comparison)
92
156
  result = comparison.different?
93
157
 
94
158
  # Cleanup after comparisons
95
159
  if !result && comparison.base_image_path.exist?
96
160
  FileUtils.mv(comparison.base_image_path, comparison.image_path, force: true)
97
- else
161
+ elsif !comparison.dimensions_changed?
98
162
  FileUtils.rm_rf(comparison.base_image_path)
99
163
  end
100
164
 
101
165
  return unless result
102
166
 
103
- "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")}"
104
168
  end
105
169
 
106
170
  private
107
171
 
172
+ def _raise_error(error_msg, backtrace)
173
+ raise CapybaraScreenshotDiff::ExpectationNotMet.new(error_msg).tap { _1.set_backtrace(backtrace) }
174
+ end
175
+
108
176
  def build_screenshot_matches_job(screenshot_full_name, options)
109
177
  ScreenshotMatcher
110
178
  .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.10.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,49 @@
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(*args, skip_stack_frames: 0, **opts)
23
+ assert_nothing_raised do
24
+ super(*args, skip_stack_frames: skip_stack_frames + 3, **opts)
25
+ end
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
+ if errors.present?
41
+ assertion = ::Minitest::Assertion.new(errors.join("\n\n"))
42
+ assertion.set_backtrace []
43
+ failures << assertion
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ 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