snap_diff-capybara 2.0.0.alpha1 → 2.0.0.beta2

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -0
  3. data/docs/UPGRADING.md +243 -1
  4. data/docs/architecture.md +91 -53
  5. data/docs/ci-integration.md +13 -3
  6. data/docs/configuration.md +31 -2
  7. data/docs/drivers.md +6 -0
  8. data/docs/framework-setup.md +21 -3
  9. data/docs/reporters.md +20 -5
  10. data/docs/snapdiff.md +326 -0
  11. data/docs/thread_safety.md +4 -3
  12. data/lib/capybara/screenshot/diff/config_legacy.rb +26 -72
  13. data/lib/capybara/screenshot/diff/image_compare.rb +5 -2
  14. data/lib/capybara/screenshot/diff/region.rb +3 -105
  15. data/lib/capybara/screenshot/diff/reporters/default.rb +4 -106
  16. data/lib/capybara_screenshot_diff/screenshot_assertion.rb +12 -24
  17. data/lib/capybara_screenshot_diff.rb +10 -4
  18. data/lib/snap_diff/area_calculator.rb +1 -3
  19. data/lib/snap_diff/browser_helpers.rb +1 -3
  20. data/lib/snap_diff/capture/viewport.rb +6 -7
  21. data/lib/snap_diff/comparison.rb +15 -28
  22. data/lib/snap_diff/config.rb +151 -29
  23. data/lib/snap_diff/deprecation.rb +26 -8
  24. data/lib/snap_diff/drivers.rb +20 -0
  25. data/lib/snap_diff/dsl.rb +12 -12
  26. data/lib/snap_diff/errors.rb +19 -0
  27. data/lib/snap_diff/integrations/cucumber.rb +5 -4
  28. data/lib/snap_diff/integrations/minitest.rb +11 -12
  29. data/lib/snap_diff/integrations/rspec.rb +7 -6
  30. data/lib/snap_diff/legacy_shims.rb +18 -0
  31. data/lib/snap_diff/region.rb +117 -0
  32. data/lib/snap_diff/reporters/default.rb +107 -0
  33. data/lib/snap_diff/reporters/html.rb +7 -9
  34. data/lib/snap_diff/reporting.rb +13 -4
  35. data/lib/snap_diff/screenshot_assertion.rb +37 -4
  36. data/lib/snap_diff/screenshot_matcher.rb +4 -4
  37. data/lib/snap_diff/screenshoter.rb +1 -1
  38. data/lib/snap_diff/snap_manager.rb +1 -2
  39. data/lib/snap_diff/stable_screenshoter.rb +2 -2
  40. data/lib/snap_diff/utils.rb +5 -3
  41. data/lib/snap_diff/version.rb +1 -1
  42. data/lib/snap_diff.rb +44 -15
  43. metadata +5 -1
@@ -6,7 +6,10 @@
6
6
  # snap_diff/comparison itself pulls in the ComparisonResult and Drivers
7
7
  # units, and the shims keep the old ::Difference / ::Drivers names
8
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.
9
+ # image_compare.rb did. The internal images-holder struct lives at
10
+ # SnapDiff::Comparison::Images (its old ::Comparison name resolves via the
11
+ # shims) and the driver cache at SnapDiff::Drivers.loaded, with
12
+ # LOADED_DRIVERS kept as an eager same-object alias by legacy_shims
13
+ # (ADR-008 step 5).
11
14
  require "snap_diff/comparison"
12
15
  require "snap_diff/legacy_shims"
@@ -1,107 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- class Region
4
- attr_accessor :x, :y, :width, :height
5
-
6
- def initialize(x, y, width, height)
7
- @x, @y, @width, @height = x, y, width, height
8
- end
9
-
10
- def self.from_edge_coordinates(left, top, right, bottom)
11
- return nil unless left && top && right && bottom
12
- return nil if right < left || bottom < top
13
-
14
- Region.new(left, top, right - left, bottom - top)
15
- end
16
-
17
- def to_edge_coordinates
18
- [left, top, right, bottom]
19
- end
20
-
21
- def to_top_left_corner_coordinates
22
- [x, y, width, height]
23
- end
24
-
25
- def top
26
- y
27
- end
28
-
29
- def bottom
30
- y + height
31
- end
32
-
33
- def left
34
- x
35
- end
36
-
37
- def right
38
- x + width
39
- end
40
-
41
- def size
42
- return 0 if width < 0 || height < 0
43
-
44
- result = width * height
45
- result.zero? ? 1 : result
46
- end
47
-
48
- def to_a
49
- [@x, @y, @width, @height]
50
- end
51
-
52
- def find_intersect_with(region)
53
- return nil unless intersect?(region)
54
-
55
- new_left = [x, region.x].max
56
- new_top = [y, region.y].max
57
-
58
- Region.new(new_left, new_top, [right, region.right].min - new_left, [bottom, region.bottom].min - new_top)
59
- end
60
-
61
- def intersect?(region)
62
- left <= region.right && right >= region.left && top <= region.bottom && bottom >= region.top
63
- end
64
-
65
- def move_by(right_by, down_by)
66
- Region.new(x + right_by, y + down_by, width, height)
67
- end
68
-
69
- def find_relative_intersect(region)
70
- intersect = find_intersect_with(region)
71
- return nil unless intersect
72
-
73
- intersect.move_by(-x, -y)
74
- end
75
-
76
- def cover?(x, y)
77
- x.between?(left, right) && y.between?(top, bottom)
78
- end
79
-
80
- def empty?
81
- width.zero? || height.zero?
82
- end
83
-
84
- def blank?
85
- empty?
86
- end
87
-
88
- def present?
89
- !empty?
90
- end
91
-
92
- def inspect
93
- "Region(x: #{x}, y: #{y}, width: #{width}, height: #{height})"
94
- end
95
-
96
- # need to add this method to make it work with assert_equal
97
- def ==(other)
98
- case other
99
- when Region
100
- x == other.x && y == other.y && width == other.width && height == other.height
101
- when Array
102
- to_a == other
103
- else
104
- false
105
- end
106
- end
107
- end
3
+ # Forwarder (ADR-008 step 3): Region now lives at SnapDiff::Region;
4
+ # snap_diff/region also defines the eager top-level `Region` alias.
5
+ require "snap_diff/region"
@@ -1,109 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "snap_diff/annotation_service"
4
- # Defines the Capybara::Screenshot::Diff namespace this file reopens.
3
+ # Forwarder (ADR-008 step 4): the default reporter lives at
4
+ # SnapDiff::Reporters::Default; the old name now resolves lazily via
5
+ # snap_diff/legacy_shims' const_missing, with a deprecation warning.
6
+ require "snap_diff/reporters/default"
5
7
  require "snap_diff/legacy_shims"
6
-
7
- module Capybara::Screenshot::Diff
8
- module Reporters
9
- class Default
10
- attr_reader :difference
11
-
12
- def initialize(difference)
13
- @difference = difference
14
- @annotation_service = SnapDiff::AnnotationService.new(difference)
15
- end
16
-
17
- def annotated_image_path
18
- annotation_service.annotated_image_path
19
- end
20
-
21
- def annotated_base_image_path
22
- annotation_service.annotated_base_image_path
23
- end
24
-
25
- def heatmap_diff_path
26
- annotation_service.heatmap_diff_path
27
- end
28
-
29
- def generate
30
- if difference.equal?
31
- # NOTE: Delete previous run runtime files
32
- clean_tmp_files
33
- return nil
34
- end
35
-
36
- if difference.failed? && difference.failed_by[:different_dimensions]
37
- return build_error_for_different_dimensions
38
- end
39
-
40
- annotate_and_save_images
41
- build_error_message
42
- end
43
-
44
- def clean_tmp_files
45
- annotation_service.clean_tmp_files
46
- end
47
-
48
- def annotate_and_save_images
49
- annotation_service.annotate_and_save_images
50
- end
51
-
52
- def save_annotation_for(image, image_path)
53
- annotation_service.save_annotation_for(image, image_path)
54
- end
55
-
56
- def annotate_difference(image, region)
57
- annotation_service.annotate_difference(image, region)
58
- end
59
-
60
- def annotate_skip_areas(image, skip_areas)
61
- annotation_service.annotate_skip_areas(image, skip_areas)
62
- end
63
-
64
- def save(image, image_path)
65
- annotation_service.save(image, image_path)
66
- end
67
-
68
- def build_error_for_different_dimensions
69
- change_msg = [comparison.base_image, comparison.new_image]
70
- .map { |image| driver.dimension(image).join("x") }
71
- .join(" => ")
72
-
73
- "Dimensions have changed: #{change_msg}\n#{base_image_path.to_path}\n#{image_path.to_path}"
74
- end
75
-
76
- NEW_LINE = "\n"
77
-
78
- def build_error_message
79
- [
80
- "(#{difference.to_h.to_json})",
81
- image_path.to_path,
82
- annotated_base_image_path.to_path,
83
- annotated_image_path.to_path,
84
- heatmap_diff_path.to_path
85
- ].join(NEW_LINE)
86
- end
87
-
88
- private
89
-
90
- attr_reader :annotation_service
91
-
92
- def base_image_path
93
- comparison.base_image_path
94
- end
95
-
96
- def image_path
97
- comparison.new_image_path
98
- end
99
-
100
- def driver
101
- @_driver ||= comparison.driver
102
- end
103
-
104
- def comparison
105
- @_comparison ||= difference.comparison
106
- end
107
- end
108
- end
109
- end
@@ -6,49 +6,37 @@ require "snap_diff/reporting"
6
6
  # warnings) via snap_diff/legacy_shims' const_missing since v2 step 6.
7
7
  require "snap_diff/legacy_shims"
8
8
 
9
- # CapybaraScreenshotDiff's module methods cover two lifecycles with
10
- # different scopes, kept separate below:
11
- #
12
- # - Session lifecycle: thread-local, per-test. The AssertionRegistry holds
13
- # the assertions and screenshot names of the test running on this thread;
14
- # `reset` clears it between tests.
15
- # - Reporter lifecycle: process-global, suite-long. Owned by
16
- # SnapDiff::Reporting; the methods here are thin public shims over it.
17
- #
18
- # `reset` is the one deliberate bridge between the two: a finished test's
19
- # assertions are handed to the reporters before the registry is cleared.
9
+ # Since ADR-008 step 6 every method here is a thin forwarder; the canonical
10
+ # homes are SnapDiff (session lifecycle: per-test, `SnapDiff.session` and
11
+ # friends) and SnapDiff::Reporting (reporter lifecycle: process-global,
12
+ # suite-long). Names, arities and object identities are unchanged -- this
13
+ # module stays as the compatibility surface for existing consumers.
20
14
  module CapybaraScreenshotDiff
21
15
  class << self
22
16
  require "forwardable"
23
17
  extend Forwardable
24
18
 
25
- # --- Session lifecycle (thread-local, per-test) ---
19
+ # --- Session lifecycle (per-test) -> SnapDiff ---
26
20
 
27
21
  def registry
28
- Thread.current[:capybara_screenshot_diff_registry] ||= SnapDiff::AssertionRegistry.new
22
+ SnapDiff.session
29
23
  end
30
24
 
31
25
  def_delegators :registry, :add_assertion, :assertions, :assertions_present?,
32
26
  :failed_assertions, :record_new_screenshot, :new_screenshots,
33
27
  :new_screenshots_present?, :screenshot_namer, :verify
34
28
 
29
+ # Written out rather than def_delegators so the arities stay 0 (a
30
+ # Forwardable-generated method takes *args, **kwargs, &block).
35
31
  def reset
36
- notify_reporters(registry.assertions)
37
- registry.reset
32
+ SnapDiff.reset
38
33
  end
39
34
 
40
- # Message to skip the test with when a new screenshot has no baseline yet
41
- # and `pending_if_new` is enabled. Adapters call this after verifying
42
- # screenshots, and skip the test with the returned message when present.
43
- #
44
- # @return [String, nil] the pending message, or nil when there is nothing to report
45
35
  def pending_screenshots_message
46
- return unless ::Capybara::Screenshot::Diff.pending_if_new && new_screenshots_present?
47
-
48
- "No baseline for: #{new_screenshots.join(", ")}. Commit the captured screenshots to record them."
36
+ SnapDiff.pending_screenshots_message
49
37
  end
50
38
 
51
- # --- Reporter lifecycle (process-global, suite-long) ---
39
+ # --- Reporter lifecycle (process-global, suite-long) -> SnapDiff::Reporting ---
52
40
 
53
41
  def reporters
54
42
  SnapDiff::Reporting.reporters
@@ -22,19 +22,25 @@ require "capybara/screenshot/diff/screenshot_matcher"
22
22
  require "capybara/screenshot/diff/reporters/default"
23
23
 
24
24
  require "capybara_screenshot_diff/error_with_filtered_backtrace"
25
+ require "snap_diff/errors"
25
26
 
26
27
  module CapybaraScreenshotDiff
27
28
  # RED_RGBA / ORANGE_RGBA moved to SnapDiff (snap_diff/annotation_service)
28
29
  # so the bare "snap_diff" entry gets them too; the old names resolve via
29
30
  # snap_diff/legacy_shims with a deprecation warning.
30
31
 
31
- class CapybaraScreenshotDiffError < SnapDiff::ErrorWithFilteredBacktrace; end
32
+ # ADR-008 step 2: the error classes moved to SnapDiff (snap_diff/errors).
33
+ # These are EAGER same-object aliases -- deliberately not const_missing
34
+ # shims -- so rescue-by-old-name and defined?/const_defined? feature
35
+ # detection keep working exactly as before (const_defined? never
36
+ # triggers const_missing).
37
+ CapybaraScreenshotDiffError = SnapDiff::Error
32
38
 
33
- class ExpectationNotMet < CapybaraScreenshotDiffError; end
39
+ ExpectationNotMet = SnapDiff::ExpectationNotMet
34
40
 
35
- class UnstableImage < CapybaraScreenshotDiffError; end
41
+ UnstableImage = SnapDiff::UnstableImage
36
42
 
37
- class WindowSizeMismatchError < SnapDiff::ErrorWithFilteredBacktrace; end
43
+ WindowSizeMismatchError = SnapDiff::WindowSizeMismatchError
38
44
  end
39
45
 
40
46
  require "capybara_screenshot_diff/dsl"
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Region has not moved yet (ADR-004 v2 scoping: it's already unnamespaced,
4
- # no v2.0 move needed).
5
- require "capybara/screenshot/diff/region"
3
+ require "snap_diff/region"
6
4
 
7
5
  module SnapDiff
8
6
  class AreaCalculator
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # Region has not moved yet (ADR-004 v2 scoping: it's already unnamespaced,
4
- # no v2.0 move needed) so this is an absolute require, not require_relative.
5
- require "capybara/screenshot/diff/region"
3
+ require "snap_diff/region"
6
4
 
7
5
  module SnapDiff
8
6
  module BrowserHelpers
@@ -9,24 +9,23 @@ module SnapDiff
9
9
  # Called exactly once per capture, before the screenshoter runs and outside
10
10
  # any stability retry loop. Today it only validates the window size
11
11
  # (raise-only, never resizes) — the same guard ScreenshotMatcher carried
12
- # inline before. v3 hangs scroll-position preservation and element-anchored
13
- # capture off this seam via the +anchor+ parameter without touching callers.
12
+ # inline before. When v3 adds scroll preservation / element-anchored
13
+ # capture it designs its own parameters here; adding an optional kwarg
14
+ # later is non-breaking, so none is reserved up front.
14
15
  module Viewport
15
16
  module_function
16
17
 
17
18
  # @param expected_window_size [Array(Integer, Integer), nil] the configured window size
18
- # @param anchor [Object, nil] reserved for v3 (scroll preservation /
19
- # element-anchored capture); accepted but unused today.
20
- # @raise [CapybaraScreenshotDiff::WindowSizeMismatchError] if the browser
19
+ # @raise [SnapDiff::WindowSizeMismatchError] if the browser
21
20
  # window does not match the expected size.
22
- def prepare!(expected_window_size, anchor: nil)
21
+ def prepare!(expected_window_size)
23
22
  return unless BrowserHelpers.window_size_is_wrong?(expected_window_size)
24
23
 
25
24
  current_size = BrowserHelpers.selenium? ?
26
25
  BrowserHelpers.session.driver.browser.manage.window.size.to_s :
27
26
  "unknown"
28
27
 
29
- raise CapybaraScreenshotDiff::WindowSizeMismatchError.new(<<~ERROR.chomp, caller)
28
+ raise SnapDiff::WindowSizeMismatchError.new(<<~ERROR.chomp, caller)
30
29
  Window size mismatch detected!
31
30
  Expected: #{expected_window_size.inspect}
32
31
  Actual: #{current_size}
@@ -6,28 +6,7 @@ require "fileutils"
6
6
  require "snap_diff/comparison_result"
7
7
  require "snap_diff/drivers"
8
8
  require "snap_diff/image_preprocessor"
9
- require "capybara/screenshot/diff/reporters/default"
10
-
11
- # The internal images-holder struct and the driver cache keep their legacy
12
- # Capybara::Screenshot::Diff homes for now: SnapDiff::Comparison is the
13
- # comparison class below (ex-ImageCompare), so the struct cannot take the
14
- # same name. Its SnapDiff home arrives only when it is folded into
15
- # Comparison as a nested value (v2 design section 2) -- renaming it here
16
- # would exceed step 5's two approved renames.
17
- module Capybara
18
- module Screenshot
19
- module Diff
20
- LOADED_DRIVERS = {}
21
-
22
- # Holds the two images (and their paths/options/driver) being compared.
23
- class Comparison < Struct.new(:new_image, :base_image, :options, :driver, :new_image_path, :base_image_path)
24
- def skip_area
25
- options[:skip_area]
26
- end
27
- end
28
- end
29
- end
30
- end
9
+ require "snap_diff/reporters/default"
31
10
 
32
11
  module SnapDiff
33
12
  # Handles comparison of two images with a focus on performance and accuracy.
@@ -53,6 +32,14 @@ module SnapDiff
53
32
  # - Only performing expensive operations when absolutely necessary
54
33
  # - Maintaining high accuracy for complex comparisons
55
34
  class Comparison
35
+ # Holds the two images (and their paths/options/driver) being compared
36
+ # (ADR-008 step 5: ex-Capybara::Screenshot::Diff::Comparison struct).
37
+ Images = Struct.new(:new_image, :base_image, :options, :driver, :new_image_path, :base_image_path) do
38
+ def skip_area
39
+ options[:skip_area]
40
+ end
41
+ end
42
+
56
43
  TOLERABLE_OPTIONS = [:tolerance, :color_distance_limit, :shift_distance_limit, :area_size_limit].freeze
57
44
 
58
45
  attr_reader :driver, :driver_options
@@ -136,7 +123,7 @@ module SnapDiff
136
123
 
137
124
  def load_images_and_build_comparison(base_path, new_path, options)
138
125
  base_img, new_img = driver.load_images(base_path, new_path)
139
- Capybara::Screenshot::Diff::Comparison.new(new_img, base_img, options, driver, new_path, base_path)
126
+ Images.new(new_img, base_img, options, driver, new_path, base_path)
140
127
  end
141
128
 
142
129
  def image_preprocessor
@@ -155,7 +142,7 @@ module SnapDiff
155
142
 
156
143
  # Analyzes the comparison and determines if images are different.
157
144
  #
158
- # @param comparison [Capybara::Screenshot::Diff::Comparison] The comparison object containing images to analyze.
145
+ # @param comparison [Comparison::Images] The comparison object containing images to analyze.
159
146
  # @param quick_mode [Boolean] When true, performs minimal checks and returns early.
160
147
  # In quick mode, returns [is_equal, difference] where:
161
148
  # - is_equal is true if images are considered equal
@@ -195,7 +182,7 @@ module SnapDiff
195
182
 
196
183
  def build_reporter
197
184
  current_difference = difference || build_null_difference
198
- Capybara::Screenshot::Diff::Reporters::Default.new(current_difference)
185
+ Reporters::Default.new(current_difference)
199
186
  end
200
187
 
201
188
  # Loads and preprocesses images for detailed comparison.
@@ -203,7 +190,7 @@ module SnapDiff
203
190
  # This method is responsible for:
204
191
  # 1. Loading both images using the configured driver
205
192
  # 2. Applying any necessary preprocessing (cropping, normalization)
206
- # 3. Creating a Capybara::Screenshot::Diff::Comparison object that holds the image data
193
+ # 3. Creating a Comparison::Images object that holds the image data
207
194
  #
208
195
  # @param base_path [String,Pathname] Path to the baseline/reference image
209
196
  # @param new_path [String,Pathname] Path to the new/candidate image
@@ -211,7 +198,7 @@ module SnapDiff
211
198
  # - :crop [Array<Integer>] Optional crop area [x, y, width, height]
212
199
  # - :skip_area [Array<Array>] Areas to exclude from comparison
213
200
  # - :tolerance [Numeric] Color tolerance threshold
214
- # @return [Capybara::Screenshot::Diff::Comparison] Prepared comparison object ready for analysis
201
+ # @return [Comparison::Images] Prepared comparison object ready for analysis
215
202
  # @raise [ArgumentError] If image files are invalid or unreadable
216
203
  def load_comparison(base_path, new_path, options)
217
204
  comparison = load_images_and_build_comparison(base_path, new_path, options)
@@ -219,7 +206,7 @@ module SnapDiff
219
206
  end
220
207
 
221
208
  def build_null_difference(failed_by = nil)
222
- comparison = Capybara::Screenshot::Diff::Comparison.new(nil, nil, driver_options, driver, image_path, base_image_path).freeze
209
+ comparison = Images.new(nil, nil, driver_options, driver, image_path, base_image_path).freeze
223
210
  ComparisonResult.build_null(comparison, base_image_path, image_path, failed_by)
224
211
  end
225
212