capybara-screenshot-diff 1.15.0 → 2.0.0.alpha1

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +59 -0
  3. data/docs/RELEASE_PREP.md +6 -6
  4. data/docs/thread_safety.md +15 -0
  5. data/lib/capybara/screenshot/diff/annotation_service.rb +5 -77
  6. data/lib/capybara/screenshot/diff/area_calculator.rb +5 -54
  7. data/lib/capybara/screenshot/diff/browser_helpers.rb +5 -119
  8. data/lib/capybara/screenshot/diff/config_legacy.rb +113 -0
  9. data/lib/capybara/screenshot/diff/cucumber.rb +1 -1
  10. data/lib/capybara/screenshot/diff/difference.rb +6 -102
  11. data/lib/capybara/screenshot/diff/drivers/base_driver.rb +7 -39
  12. data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +5 -299
  13. data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +5 -169
  14. data/lib/capybara/screenshot/diff/drivers.rb +7 -14
  15. data/lib/capybara/screenshot/diff/image_compare.rb +10 -228
  16. data/lib/capybara/screenshot/diff/image_preprocessor.rb +5 -70
  17. data/lib/capybara/screenshot/diff/os.rb +8 -13
  18. data/lib/capybara/screenshot/diff/reporters/default.rb +5 -3
  19. data/lib/capybara/screenshot/diff/screenshot_matcher.rb +5 -129
  20. data/lib/capybara/screenshot/diff/screenshoter.rb +5 -127
  21. data/lib/capybara/screenshot/diff/stable_screenshoter.rb +5 -104
  22. data/lib/capybara/screenshot/diff/utils.rb +5 -40
  23. data/lib/capybara/screenshot/diff/vcs.rb +5 -37
  24. data/lib/capybara/screenshot/diff/version.rb +7 -1
  25. data/lib/capybara_screenshot_diff/attempts_reporter.rb +5 -47
  26. data/lib/capybara_screenshot_diff/cucumber.rb +5 -17
  27. data/lib/capybara_screenshot_diff/dsl.rb +7 -129
  28. data/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb +6 -30
  29. data/lib/capybara_screenshot_diff/minitest.rb +6 -56
  30. data/lib/capybara_screenshot_diff/reporters/html.rb +5 -135
  31. data/lib/capybara_screenshot_diff/rspec.rb +5 -63
  32. data/lib/capybara_screenshot_diff/screenshot_assertion.rb +39 -157
  33. data/lib/capybara_screenshot_diff/screenshot_namer.rb +5 -79
  34. data/lib/capybara_screenshot_diff/snap.rb +5 -64
  35. data/lib/capybara_screenshot_diff/snap_manager.rb +5 -82
  36. data/lib/capybara_screenshot_diff/static.rb +3 -5
  37. data/lib/capybara_screenshot_diff.rb +29 -100
  38. data/lib/snap_diff/annotation_service.rb +84 -0
  39. data/lib/snap_diff/area_calculator.rb +56 -0
  40. data/lib/snap_diff/attempts_reporter.rb +51 -0
  41. data/lib/snap_diff/browser_helpers.rb +121 -0
  42. data/lib/snap_diff/capture/viewport.rb +40 -0
  43. data/lib/snap_diff/comparison.rb +238 -0
  44. data/lib/snap_diff/comparison_result.rb +104 -0
  45. data/lib/snap_diff/config.rb +6 -2
  46. data/lib/snap_diff/deprecation.rb +84 -0
  47. data/lib/snap_diff/driver.rb +37 -0
  48. data/lib/snap_diff/drivers/chunky_png_driver.rb +298 -0
  49. data/lib/snap_diff/drivers/vips_driver.rb +171 -0
  50. data/lib/snap_diff/drivers.rb +14 -0
  51. data/lib/snap_diff/dsl.rb +143 -0
  52. data/lib/snap_diff/error_with_filtered_backtrace.rb +32 -0
  53. data/lib/snap_diff/image_preprocessor.rb +68 -0
  54. data/lib/snap_diff/integrations/cucumber.rb +22 -0
  55. data/lib/snap_diff/integrations/minitest.rb +70 -0
  56. data/lib/snap_diff/integrations/rspec.rb +68 -0
  57. data/lib/snap_diff/legacy_shims.rb +99 -0
  58. data/lib/snap_diff/os.rb +17 -0
  59. data/lib/snap_diff/reporters/html.rb +143 -0
  60. data/lib/snap_diff/reporting.rb +53 -0
  61. data/lib/snap_diff/screenshot_assertion.rb +143 -0
  62. data/lib/snap_diff/screenshot_matcher.rb +113 -0
  63. data/lib/snap_diff/screenshot_namer.rb +81 -0
  64. data/lib/snap_diff/screenshoter.rb +129 -0
  65. data/lib/snap_diff/snap.rb +66 -0
  66. data/lib/snap_diff/snap_manager.rb +125 -0
  67. data/lib/snap_diff/stable_screenshoter.rb +103 -0
  68. data/lib/snap_diff/static.rb +11 -0
  69. data/lib/snap_diff/utils.rb +38 -0
  70. data/lib/snap_diff/vcs.rb +35 -0
  71. data/lib/snap_diff/version.rb +5 -0
  72. data/lib/snap_diff.rb +22 -6
  73. metadata +36 -2
  74. /data/lib/{capybara_screenshot_diff → snap_diff}/reporters/templates/report.html.erb +0 -0
@@ -1,230 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "pathname"
4
- require "fileutils"
5
-
6
- require "capybara/screenshot/diff/difference"
7
- require "capybara/screenshot/diff/drivers"
8
- require "capybara/screenshot/diff/image_preprocessor"
9
- require "capybara/screenshot/diff/reporters/default"
10
-
11
- module Capybara
12
- module Screenshot
13
- module Diff
14
- LOADED_DRIVERS = {}
15
-
16
- # Holds the two images (and their paths/options/driver) being compared.
17
- class Comparison < Struct.new(:new_image, :base_image, :options, :driver, :new_image_path, :base_image_path)
18
- def skip_area
19
- options[:skip_area]
20
- end
21
- end
22
-
23
- # Handles comparison of two images with a focus on performance and accuracy.
24
- #
25
- # This class implements a multi-layered optimization strategy for image comparison:
26
- #
27
- # 1. Early File-based Checks (Fastest):
28
- # - Verifies both images exist (raises ArgumentError if not)
29
- # - Compares file sizes (different sizes → different images)
30
- # - Performs byte-by-byte comparison for identical files (exact match)
31
- #
32
- # 2. Quick Comparison (Fast):
33
- # - Compares image dimensions (different dimensions → different images)
34
- # - Performs pixel-by-pixel comparison if dimensions match
35
- #
36
- # 3. Detailed Analysis (Slower):
37
- # - Only performed if quick comparison finds differences
38
- # - Handles anti-aliasing, color tolerance, and shift detection
39
- # - Respects skip_area and other comparison parameters
40
- #
41
- # This layered approach ensures optimal performance by:
42
- # - Using the fastest possible method for early rejection
43
- # - Only performing expensive operations when absolutely necessary
44
- # - Maintaining high accuracy for complex comparisons
45
- class ImageCompare
46
- TOLERABLE_OPTIONS = [:tolerance, :color_distance_limit, :shift_distance_limit, :area_size_limit].freeze
47
-
48
- attr_reader :driver, :driver_options
49
- attr_reader :image_path, :base_image_path
50
- attr_reader :difference, :error_message
51
-
52
- def initialize(image_path, base_image_path, options = {})
53
- @image_path = Pathname.new(image_path)
54
- @base_image_path = Pathname.new(base_image_path)
55
-
56
- ensure_files_exist!
57
-
58
- @driver_options = options.freeze
59
- @driver = Drivers.for(@driver_options)
60
- @without_tolerable_options = (driver_options.keys & TOLERABLE_OPTIONS).empty?
61
- end
62
-
63
- # Performs a quick comparison of two image files.
64
- #
65
- # This method is optimized for speed and will return as soon as a difference is found.
66
- # It's used for fast rejection before performing more expensive comparisons.
67
- #
68
- # @return [Boolean]
69
- # - `true` if images are exactly identical (byte-for-byte match)
70
- # - `false` if images are different or if a quick difference is detected
71
- #
72
- # @note This method will raise ArgumentError if either image file is missing.
73
- def quick_equal?
74
- if base_image_path.size == image_path.size
75
- return true if files_identical?(base_image_path, image_path)
76
- end
77
-
78
- result, difference = find_difference(quick_mode: true)
79
- self.difference = difference
80
- result
81
- end
82
-
83
- def ensure_files_exist!
84
- raise ArgumentError, "There is no original (base) screenshot located at #{@base_image_path}" unless @base_image_path.exist?
85
- raise ArgumentError, "There is no new screenshot located at #{@image_path}" unless @image_path.exist?
86
- end
87
-
88
- # Determines if the images are different according to the comparison rules.
89
- #
90
- # This method performs a full comparison if not already done, including any
91
- # configured tolerances for color differences and shift distances.
92
- #
93
- # @return [Boolean]
94
- # - `true` if the images are different beyond configured tolerances
95
- # - `false` if the images are considered identical
96
- #
97
- # @see #processed
98
- # @see #analyze_difference
99
- def different?
100
- processed.difference.different?
101
- end
102
-
103
- def dimensions_changed?
104
- difference.failed_by&.[](:different_dimensions)
105
- end
106
-
107
- def reporter
108
- @reporter ||= build_reporter
109
- end
110
-
111
- def processed?
112
- !!difference
113
- end
114
-
115
- def processed
116
- self.difference = find_difference(quick_mode: false) unless processed?
117
- @error_message ||= reporter.generate
118
- self
119
- end
120
-
121
- private
122
-
123
- def without_tolerable_options?
124
- @without_tolerable_options
125
- end
126
-
127
- def load_images_and_build_comparison(base_path, new_path, options)
128
- base_img, new_img = driver.load_images(base_path, new_path)
129
- Comparison.new(new_img, base_img, options, driver, new_path, base_path)
130
- end
131
-
132
- def image_preprocessor
133
- @image_preprocessor ||= ImagePreprocessor.new(driver, driver_options)
134
- end
135
-
136
- def find_difference(quick_mode: false)
137
- # Validate images exist
138
- return build_null_difference("missing_image") unless images_exist?
139
-
140
- # Create comparison with preprocessed images
141
- comparison = load_comparison(base_image_path, image_path, driver_options)
142
-
143
- analyze_difference(comparison, quick_mode: quick_mode)
144
- end
145
-
146
- # Analyzes the comparison and determines if images are different.
147
- #
148
- # @param comparison [Comparison] The comparison object containing images to analyze.
149
- # @param quick_mode [Boolean] When true, performs minimal checks and returns early.
150
- # In quick mode, returns [is_equal, difference] where:
151
- # - is_equal is true if images are considered equal
152
- # - difference is a Difference object or nil
153
- # When false, returns a Difference object directly.
154
- # @return [Array, Difference] Result format depends on quick_mode parameter.
155
- def analyze_difference(comparison, quick_mode: true)
156
- # Handle dimension differences
157
- unless driver.same_dimension?(comparison)
158
- result = Difference.build_null(comparison, comparison.base_image_path, comparison.new_image_path, {different_dimensions: true})
159
- return quick_mode ? [false, result] : result
160
- end
161
-
162
- # Handle identical pixels
163
- if driver.same_pixels?(comparison)
164
- result = Difference.build_null(comparison, comparison.base_image_path, comparison.new_image_path)
165
- return quick_mode ? [true, result] : result
166
- end
167
-
168
- # Handle early return for non-tolerable options
169
- if quick_mode && without_tolerable_options?
170
- return [false, nil]
171
- end
172
-
173
- # Process difference region
174
- region = driver.find_difference_region(comparison)
175
-
176
- # Only create a proper difference object if we've completed the comparison
177
- quick_mode ? [!region.different?, region] : region
178
- end
179
-
180
- def difference=(new_difference)
181
- @error_message = nil
182
- @reporter = nil
183
- @difference = new_difference
184
- end
185
-
186
- def build_reporter
187
- current_difference = difference || build_null_difference
188
- Reporters::Default.new(current_difference)
189
- end
190
-
191
- # Loads and preprocesses images for detailed comparison.
192
- #
193
- # This method is responsible for:
194
- # 1. Loading both images using the configured driver
195
- # 2. Applying any necessary preprocessing (cropping, normalization)
196
- # 3. Creating a Comparison object that holds the image data
197
- #
198
- # @param base_path [String,Pathname] Path to the baseline/reference image
199
- # @param new_path [String,Pathname] Path to the new/candidate image
200
- # @param options [Hash] Comparison options including:
201
- # - :crop [Array<Integer>] Optional crop area [x, y, width, height]
202
- # - :skip_area [Array<Array>] Areas to exclude from comparison
203
- # - :tolerance [Numeric] Color tolerance threshold
204
- # @return [Comparison] Prepared comparison object ready for analysis
205
- # @raise [ArgumentError] If image files are invalid or unreadable
206
- def load_comparison(base_path, new_path, options)
207
- comparison = load_images_and_build_comparison(base_path, new_path, options)
208
- image_preprocessor.process_comparison(comparison)
209
- end
210
-
211
- def build_null_difference(failed_by = nil)
212
- comparison = Comparison.new(nil, nil, driver_options, driver, image_path, base_image_path).freeze
213
- Difference.build_null(comparison, base_image_path, image_path, failed_by)
214
- end
215
-
216
- # Check if both images exist
217
- def images_exist?
218
- base_image_path.exist? && image_path.exist?
219
- end
220
-
221
- # Check if files are identical by content
222
- def files_identical?(file1, file2)
223
- FileUtils.compare_file(file1, file2)
224
- rescue SystemCallError, IOError
225
- false
226
- end
227
- end
228
- end
229
- end
230
- end
3
+ # Forwarder (ADR-004 v2 step 6): the comparison class lives at
4
+ # SnapDiff::Comparison (ex-ImageCompare); the old name now resolves lazily
5
+ # via snap_diff/legacy_shims' const_missing, with a deprecation warning.
6
+ # snap_diff/comparison itself pulls in the ComparisonResult and Drivers
7
+ # units, and the shims keep the old ::Difference / ::Drivers names
8
+ # resolvable, so this path still provides everything the pre-move
9
+ # image_compare.rb did. The internal Comparison struct and LOADED_DRIVERS
10
+ # keep their legacy names and are defined by snap_diff/comparison.rb itself.
11
+ require "snap_diff/comparison"
12
+ require "snap_diff/legacy_shims"
@@ -1,72 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Capybara
4
- module Screenshot
5
- module Diff
6
- # Handles image preprocessing operations (skip_area and median filtering)
7
- #
8
- # This class applies preprocessing filters to images before comparison,
9
- # such as masking specific regions (skip_area) or applying noise reduction.
10
- # It's designed to work with either direct image objects or with options.
11
- class ImagePreprocessor
12
- attr_reader :driver, :options
13
-
14
- def initialize(driver, options = {})
15
- @driver = driver
16
- @options = options
17
- end
18
-
19
- # Process a comparison object directly
20
- # This allows reusing the comparison's existing options
21
- # @param [Comparison] comparison the comparison object
22
- # @return [Comparison] the comparison object
23
- def process_comparison(comparison)
24
- # Process both images
25
- comparison.base_image = process_image(comparison.base_image, comparison.base_image_path)
26
- comparison.new_image = process_image(comparison.new_image, comparison.new_image_path)
27
-
28
- comparison
29
- end
30
-
31
- private
32
-
33
- def process_image(image, path)
34
- result = image
35
- result = apply_skip_area(result) if skip_area
36
- result = apply_median_filter(result, path) if median_filter_window_size
37
- result
38
- end
39
-
40
- def apply_skip_area(image)
41
- skip_area.reduce(image) do |result, region|
42
- driver.add_black_box(result, region)
43
- end
44
- end
45
-
46
- def apply_median_filter(image, path)
47
- if driver.supports?(:filter_image_with_median)
48
- driver.filter_image_with_median(image, median_filter_window_size)
49
- else
50
- warn_about_skipped_median_filter(path)
51
- image
52
- end
53
- end
54
-
55
- def warn_about_skipped_median_filter(path)
56
- warn(
57
- "[capybara-screenshot-diff] Median filter has been skipped for #{path} " \
58
- "because it is not supported by #{driver.class}"
59
- )
60
- end
61
-
62
- def skip_area
63
- options[:skip_area]
64
- end
65
-
66
- def median_filter_window_size
67
- options[:median_filter_window_size]
68
- end
69
- end
70
- end
71
- end
72
- end
3
+ # Forwarder (ADR-004 v2 step 6): Capybara::Screenshot::Diff::ImagePreprocessor
4
+ # now resolves lazily via snap_diff/legacy_shims' const_missing, with a
5
+ # deprecation warning pointing at SnapDiff::ImagePreprocessor.
6
+ require "snap_diff/image_preprocessor"
7
+ require "snap_diff/legacy_shims"
@@ -1,19 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "snap_diff/os"
4
+
5
+ # Deliberately EAGER and silent (v2 step 6 exception): Os is an advertised
6
+ # entry-point constant probed with Object.const_defined? by
7
+ # support_load_probe_test.rb, and const_defined? never triggers
8
+ # const_missing -- a lazy shim would break that contract. See
9
+ # snap_diff/legacy_shims.rb for the full exception list.
3
10
  module Capybara
4
11
  module Screenshot
5
- module Os
6
- ON_WINDOWS = !!(RbConfig::CONFIG["host_os"] =~ /mswin|mingw|cygwin/)
7
- ON_MAC = !!(RbConfig::CONFIG["host_os"] =~ /darwin/)
8
- ON_LINUX = !!(RbConfig::CONFIG["host_os"] =~ /linux/)
9
-
10
- def self.name
11
- return "windows" if ON_WINDOWS
12
- return "macos" if ON_MAC
13
- return "linux" if ON_LINUX
14
-
15
- "unknown"
16
- end
17
- end
12
+ Os = SnapDiff::Os
18
13
  end
19
14
  end
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "capybara/screenshot/diff/annotation_service"
3
+ require "snap_diff/annotation_service"
4
+ # Defines the Capybara::Screenshot::Diff namespace this file reopens.
5
+ require "snap_diff/legacy_shims"
4
6
 
5
7
  module Capybara::Screenshot::Diff
6
8
  module Reporters
@@ -9,7 +11,7 @@ module Capybara::Screenshot::Diff
9
11
 
10
12
  def initialize(difference)
11
13
  @difference = difference
12
- @annotation_service = AnnotationService.new(difference)
14
+ @annotation_service = SnapDiff::AnnotationService.new(difference)
13
15
  end
14
16
 
15
17
  def annotated_image_path
@@ -75,7 +77,7 @@ module Capybara::Screenshot::Diff
75
77
 
76
78
  def build_error_message
77
79
  [
78
- "(#{difference.inspect})",
80
+ "(#{difference.to_h.to_json})",
79
81
  image_path.to_path,
80
82
  annotated_base_image_path.to_path,
81
83
  annotated_image_path.to_path,
@@ -1,131 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "capybara_screenshot_diff/snap_manager"
4
- require_relative "screenshoter"
5
- require_relative "stable_screenshoter"
6
- require_relative "browser_helpers"
7
- require_relative "vcs"
8
- require_relative "area_calculator"
9
-
10
- module Capybara
11
- module Screenshot
12
- module Diff
13
- class ScreenshotMatcher
14
- attr_reader :screenshot_full_name, :driver_options, :screenshot_format
15
-
16
- def initialize(screenshot_full_name, options = {})
17
- @screenshot_full_name = screenshot_full_name
18
- @driver_options = Diff.default_options.merge(options)
19
-
20
- @screenshot_format = @driver_options[:screenshot_format]
21
- @snapshot = CapybaraScreenshotDiff::SnapManager.snapshot(screenshot_full_name, @screenshot_format)
22
- end
23
-
24
- def build_screenshot_assertion(skip_stack_frames: 0)
25
- check_window_size!
26
- prepare_screenshot_options
27
- check_base_screenshot
28
-
29
- capture_options, comparison_options = extract_capture_and_comparison_options!(driver_options)
30
-
31
- capture_screenshot(capture_options, comparison_options)
32
-
33
- # Pre-computation: No need to compare without base screenshot
34
- # NOTE: Consider to return PreValid Assertion Value Object with hard coded valid result
35
- unless need_to_compare?
36
- CapybaraScreenshotDiff.record_new_screenshot(screenshot_full_name)
37
- return
38
- end
39
-
40
- create_screenshot_assertion(skip_stack_frames + 1, comparison_options)
41
- end
42
-
43
- # Captures a screenshot without comparing it to a baseline.
44
- def capture
45
- check_window_size!
46
- prepare_screenshot_options
47
-
48
- capture_options, comparison_options = extract_capture_and_comparison_options!(driver_options)
49
-
50
- @snapshot.manager.create_output_directory_for(@snapshot.path)
51
- capture_screenshot(capture_options, comparison_options)
52
- end
53
-
54
- private
55
-
56
- def need_to_compare?
57
- @snapshot.base_path.exist?
58
- end
59
-
60
- def check_window_size!
61
- if BrowserHelpers.window_size_is_wrong?(Screenshot.window_size)
62
- current_size = BrowserHelpers.selenium? ?
63
- BrowserHelpers.session.driver.browser.manage.window.size.to_s :
64
- "unknown"
65
-
66
- raise CapybaraScreenshotDiff::WindowSizeMismatchError.new(<<~ERROR.chomp, caller)
67
- Window size mismatch detected!
68
- Expected: #{Screenshot.window_size.inspect}
69
- Actual: #{current_size}
70
-
71
- Screenshots cannot be compared when window sizes don't match.
72
- Please ensure the browser window is properly sized before taking screenshots.
73
- ERROR
74
- end
75
- end
76
-
77
- def prepare_screenshot_options
78
- area_calculator = AreaCalculator.new(driver_options.delete(:crop), driver_options[:skip_area])
79
-
80
- driver_options[:crop] = area_calculator.calculate_crop
81
- driver_options[:skip_area] = area_calculator.calculate_skip_area
82
- driver_options[:driver] = Drivers.for(driver_options[:driver])
83
- end
84
-
85
- def check_base_screenshot
86
- @snapshot.checkout_base_screenshot
87
-
88
- if Capybara::Screenshot::Diff.fail_if_new && !@snapshot.base_path.exist?
89
- raise CapybaraScreenshotDiff::ExpectationNotMet.new(<<~ERROR.chomp, caller)
90
- No existing screenshot found for #{@snapshot.base_path}!
91
- To record baselines: RECORD_SCREENSHOTS=1 bundle exec rake test
92
- To allow new screenshots: Capybara::Screenshot::Diff.fail_if_new = false
93
- ERROR
94
- end
95
- end
96
-
97
- def capture_screenshot(capture_options, comparison_options)
98
- screenshoter = if capture_options[:stability_time_limit]
99
- StableScreenshoter.new(capture_options, comparison_options)
100
- else
101
- Diff.screenshoter.new(capture_options, comparison_options)
102
- end
103
- screenshoter.take_comparison_screenshot(@snapshot)
104
- end
105
-
106
- def create_screenshot_assertion(skip_stack_frames, comparison_options)
107
- assertion = CapybaraScreenshotDiff::ScreenshotAssertion.new(screenshot_full_name)
108
- assertion.caller = caller(skip_stack_frames + 1)
109
- assertion.compare = ImageCompare.new(@snapshot.path, @snapshot.base_path, comparison_options)
110
- assertion
111
- end
112
-
113
- def extract_capture_and_comparison_options!(driver_options = {})
114
- [
115
- {
116
- # screenshot options
117
- capybara_screenshot_options: driver_options[:capybara_screenshot_options],
118
- crop: driver_options.delete(:crop),
119
- # delivery options
120
- screenshot_format: driver_options[:screenshot_format],
121
- # stability options
122
- stability_time_limit: driver_options.delete(:stability_time_limit),
123
- wait: driver_options.delete(:wait)
124
- },
125
- driver_options
126
- ]
127
- end
128
- end
129
- end
130
- end
131
- end
3
+ # Forwarder (ADR-004 v2 step 6): Capybara::Screenshot::Diff::ScreenshotMatcher
4
+ # now resolves lazily via snap_diff/legacy_shims' const_missing, with a
5
+ # deprecation warning pointing at SnapDiff::ScreenshotMatcher.
6
+ require "snap_diff/screenshot_matcher"
7
+ require "snap_diff/legacy_shims"
@@ -1,129 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "os"
4
- require_relative "browser_helpers"
5
-
6
- module Capybara
7
- module Screenshot
8
- class Screenshoter
9
- attr_reader :capture_options, :driver
10
-
11
- # @param capture_options [Hash] Options for capturing (window_size, wait, etc.)
12
- # @param comparison_options [Hash] Options for image comparison (driver, tolerance, etc.)
13
- def initialize(capture_options, comparison_options = {})
14
- @capture_options = capture_options
15
- @driver = Diff::Drivers.for(comparison_options)
16
- end
17
-
18
- def crop
19
- @capture_options[:crop]
20
- end
21
-
22
- def wait
23
- @capture_options[:wait]
24
- end
25
-
26
- def capybara_screenshot_options
27
- @capture_options[:capybara_screenshot_options] || {}
28
- end
29
-
30
- # Try to get screenshot from browser.
31
- # On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts
32
- # On reaching `wait` limit then it has been failed. On failing we annotate screenshot attempts to help to debug
33
- def take_comparison_screenshot(snapshot)
34
- capture_screenshot_at(snapshot)
35
- snapshot.cleanup_attempts!
36
- end
37
-
38
- PNG_EXTENSION = ".png"
39
-
40
- def take_screenshot(screenshot_path)
41
- blurred_input = prepare_page_for_screenshot(timeout: wait)
42
-
43
- # Take browser screenshot and save
44
- save_and_process_screenshot(screenshot_path)
45
-
46
- blurred_input&.click
47
- end
48
-
49
- def process_screenshot(stored_path, screenshot_path)
50
- screenshot_image = driver.from_file(stored_path)
51
-
52
- # TODO(uwe): Remove when chromedriver takes right size screenshots
53
- # TODO: Adds tests when this case is true
54
- screenshot_image = resize_if_needed(screenshot_image) if selenium_with_retina_screen?
55
- # ODOT
56
-
57
- screenshot_image = driver.crop(crop, screenshot_image) if crop
58
-
59
- driver.save_image_to(screenshot_image, screenshot_path)
60
- end
61
-
62
- def notice_how_to_avoid_this
63
- unless defined?(@_csd_retina_warned)
64
- warn "Halving retina screenshot. " \
65
- 'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
66
- @_csd_retina_warned = true
67
- end
68
- end
69
-
70
- def prepare_page_for_screenshot(timeout:)
71
- wait_images_loaded(timeout: timeout) if timeout
72
-
73
- blurred_input = BrowserHelpers.blur_from_focused_element if Screenshot.blur_active_element
74
-
75
- BrowserHelpers.hide_caret if Screenshot.hide_caret
76
- BrowserHelpers.disable_animations if Screenshot.disable_animations
77
-
78
- blurred_input
79
- end
80
-
81
- def wait_images_loaded(timeout:)
82
- return unless timeout
83
-
84
- deadline_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
85
- loop do
86
- pending_image = BrowserHelpers.pending_image_to_load
87
- break unless pending_image
88
-
89
- if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline_at
90
- raise CapybaraScreenshotDiff::ExpectationNotMet.new("Images have not been loaded after #{timeout}s: #{pending_image.inspect}", caller)
91
- end
92
-
93
- sleep 0.025
94
- end
95
- end
96
-
97
- private
98
-
99
- def save_and_process_screenshot(screenshot_path)
100
- tmpfile = Tempfile.new([screenshot_path.basename.to_s, PNG_EXTENSION])
101
- BrowserHelpers.session.save_screenshot(tmpfile.path, **capybara_screenshot_options)
102
- # Load saved screenshot and pre-process it
103
- process_screenshot(tmpfile.path, screenshot_path)
104
- ensure
105
- tmpfile&.close!
106
- end
107
-
108
- def capture_screenshot_at(snapshot)
109
- take_screenshot(snapshot.next_attempt_path!)
110
-
111
- snapshot.commit_last_attempt
112
- end
113
-
114
- def resize_if_needed(saved_image)
115
- expected_image_width = Screenshot.window_size[0]
116
- return saved_image if driver.width_for(saved_image) < expected_image_width * 2
117
-
118
- notice_how_to_avoid_this
119
-
120
- new_height = expected_image_width * driver.height_for(saved_image) / driver.width_for(saved_image)
121
- driver.resize_image_to(saved_image, expected_image_width, new_height)
122
- end
123
-
124
- def selenium_with_retina_screen?
125
- Os::ON_MAC && BrowserHelpers.selenium? && Screenshot.window_size
126
- end
127
- end
128
- end
129
- end
3
+ # Forwarder (ADR-004 v2 step 6): Capybara::Screenshot::Screenshoter now
4
+ # resolves lazily via snap_diff/legacy_shims' const_missing, with a
5
+ # deprecation warning pointing at SnapDiff::Screenshoter.
6
+ require "snap_diff/screenshoter"
7
+ require "snap_diff/legacy_shims"