snap_diff-capybara 2.0.0.beta1 → 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 +56 -0
  3. data/docs/UPGRADING.md +19 -4
  4. data/docs/architecture.md +38 -22
  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
@@ -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
 
@@ -1,35 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The MAPPING below references the legacy modules, so they must be loaded
4
- # first. Requires config_legacy directly (a leaf, see its own header
5
- # comment), not the capybara_screenshot_diff umbrella -- config.rb is
6
- # required by both snap_diff.rb and (indirectly) capybara_screenshot_diff.rb,
7
- # and neither of those may lead back here.
8
- require "capybara/screenshot/diff/config_legacy"
3
+ require "pathname"
4
+
5
+ # This file is the LEAF of the config require graph (ADR-008 step 1):
6
+ # config_legacy.rb requires it, so it must never require config_legacy nor
7
+ # anything that leads back to either entry point. The MAPPING below needs
8
+ # the legacy module constants to exist at class-body eval time, so the empty
9
+ # skeleton is predefined here (same technique as legacy_shims.rb);
10
+ # config_legacy.rb reopens these modules and installs the delegating
11
+ # accessors from MAPPING.
12
+ module Capybara
13
+ module Screenshot
14
+ module Diff
15
+ end
16
+ end
17
+ end
18
+
19
+ # Referenced by Config#initialize (screenshoter/manager defaults), which
20
+ # runs at the eager Config.new at the bottom of this file, so they must be
21
+ # real, already-loaded classes first. Neither requires back here.
22
+ require "snap_diff/screenshoter"
23
+ require "snap_diff/snap_manager"
9
24
 
10
25
  module SnapDiff
11
- # Flat, additive consolidation of every existing
12
- # +Capybara::Screenshot+ / +Capybara::Screenshot::Diff+ +mattr_accessor+
13
- # setting behind one object: <tt>SnapDiff.config.<em>attr</em></tt>.
26
+ # Flat consolidation of every legacy +Capybara::Screenshot+ /
27
+ # +Capybara::Screenshot::Diff+ setting behind one object:
28
+ # <tt>SnapDiff.config.<em>attr</em></tt>.
29
+ #
30
+ # Storage ownership (ADR-008 step 1, inverted from the original v2
31
+ # consolidation): Config IS the single storage. The legacy accessors on
32
+ # +Capybara::Screenshot+ / +Capybara::Screenshot::Diff+ are thin
33
+ # delegators installed by config_legacy.rb from {MAPPING} -- one storage,
34
+ # two views, so a write through either surface is visible through the
35
+ # other structurally, not by synchronization.
14
36
  #
15
- # Storage ownership: the OLD mattr_accessors remain the single source of
16
- # truth. Config holds no state of its own -- every reader/writer defined
17
- # from {MAPPING} simply forwards to the existing accessor. This is
18
- # deliberate, not just simplest: several of those accessors carry default
19
- # logic with observable timing (+fail_if_new+ derives its default from
20
- # +ENV["CI"]+, +root+ falls back to +Rails.root+) that Rails' +mattr_accessor+
21
- # evaluates once, at class-body-eval time, when +capybara_screenshot_diff.rb+
22
- # first loads. Re-implementing that logic here -- even faithfully -- would
23
- # mean evaluating it again, at a different moment (Config#new time), which
24
- # is exactly the kind of "when defaults are evaluated" divergence the v2
25
- # consolidation must not introduce. Delegating sidesteps the question
26
- # entirely: Config never evaluates a default, it only ever forwards to
27
- # whichever accessor already owns one. Bidirectional consistency (a write
28
- # through either the old accessor or Config is visible through the other)
29
- # falls out for free, because both paths read and write the exact same
30
- # class-variable-backed storage.
37
+ # Default timing contract (pinned by config_default_timing_test.rb):
38
+ # every default below is evaluated ONCE, in #initialize, which runs at
39
+ # require time of this file (the eager +Config.new+ at the bottom) -- the
40
+ # same load moment the old +mattr_accessor+ default blocks evaluated at.
41
+ # In particular +fail_if_new+ (from <tt>ENV["CI"]</tt>) and +root+ (from
42
+ # +Rails.root+ / pwd) must never become lazy read-time defaults, memoized
43
+ # or not. The one deliberately LIVE value, +default_options[:wait]+, is
44
+ # not storage at all: it stays a method-body read of
45
+ # +Capybara.default_max_wait_time+ in +#default_options+.
31
46
  class Config
32
- # config attr name => [owning module, mattr_accessor name].
47
+ # config attr name => [legacy module, legacy accessor name].
33
48
  #
34
49
  # The two names differ only for +screenshot_enabled+:
35
50
  # +Capybara::Screenshot.enabled+ and +Capybara::Screenshot::Diff.enabled+
@@ -70,9 +85,116 @@ module SnapDiff
70
85
  manager: [Capybara::Screenshot::Diff, :manager]
71
86
  }.freeze
72
87
 
73
- MAPPING.each do |name, (mod, mattr)|
74
- define_method(name) { mod.public_send(mattr) }
75
- define_method(:"#{name}=") { |value| mod.public_send(:"#{mattr}=", value) }
88
+ attr_accessor(*(MAPPING.keys - [:root]))
89
+ attr_reader :root
90
+
91
+ def initialize
92
+ # Every mapped setting gets its ivar up front (nil-defaulted ones
93
+ # included) so the full set always exists -- test_helper's per-test
94
+ # isolation snapshots/restores config by instance variable, and an
95
+ # ivar that only appears on first write would escape that snapshot
96
+ # and leak between tests.
97
+ MAPPING.each_key { |key| instance_variable_set(:"@#{key}", nil) }
98
+ # Capybara::Screenshot side.
99
+ @blur_active_element = true
100
+ @hide_caret = true
101
+ # Raw Rails.root (no coercion), matching the old mattr_reader default;
102
+ # only the writer below coerces.
103
+ @root = (defined?(Rails) && defined?(Rails.root) && Rails.root) || Pathname(".").expand_path
104
+ @save_path = "doc/screenshots"
105
+ @screenshot_format = "png"
106
+ @capybara_screenshot_options = {}
107
+ # Capybara::Screenshot::Diff side.
108
+ @delayed = true
109
+ @fail_if_new = !ENV["CI"].nil? && !ENV["CI"].empty?
110
+ @pending_if_new = false
111
+ @fail_on_difference = true
112
+ @enabled = true
113
+ @driver = :auto
114
+ @screenshoter = SnapDiff::Screenshoter
115
+ @manager = SnapDiff::SnapManager
116
+ end
117
+
118
+ def root=(path)
119
+ @root = Pathname(path).expand_path
120
+ end
121
+
122
+ # --- Derived config (ADR-008 step 7b) -------------------------------
123
+ # Read-only values computed from the storage above. They used to live
124
+ # on the legacy modules; those now one-line forward here.
125
+
126
+ # ex +Capybara::Screenshot.active?+. The two +enabled+ settings are
127
+ # independent (see {MAPPING}): the Screenshot-side one wins whenever it
128
+ # was set at all, and only a nil there falls through to the Diff-side
129
+ # one.
130
+ def active?
131
+ screenshot_enabled || (screenshot_enabled.nil? && enabled)
132
+ end
133
+
134
+ # ex +Capybara::Screenshot.screenshot_area+: the save_path, optionally
135
+ # segmented per OS and per Capybara driver.
136
+ def screenshot_area
137
+ parts = [save_path]
138
+ parts << Os.name if add_os_path
139
+ parts << Capybara.current_driver.to_s if add_driver_path
140
+ File.join(*parts)
141
+ end
142
+
143
+ # ex +Capybara::Screenshot.screenshot_area_abs+.
144
+ def screenshot_area_abs
145
+ root / screenshot_area
146
+ end
147
+
148
+ # ex +Capybara::Screenshot::Diff.default_options+: the capture/compare
149
+ # defaults handed to {SnapDiff::Comparison}. Carries the one literal
150
+ # that is not a stored setting -- the vips tolerance floor.
151
+ def default_options
152
+ {
153
+ area_size_limit: area_size_limit,
154
+ color_distance_limit: color_distance_limit,
155
+ driver: driver,
156
+ screenshot_format: screenshot_format,
157
+ capybara_screenshot_options: capybara_screenshot_options,
158
+ perceptual_threshold: perceptual_threshold,
159
+ shift_distance_limit: shift_distance_limit,
160
+ skip_area: skip_area,
161
+ stability_time_limit: stability_time_limit,
162
+ tolerance: tolerance || ((driver == :vips) ? 0.001 : nil),
163
+ # Deliberately LIVE (pinned by config_default_timing_test.rb):
164
+ # read at call time, never frozen into storage.
165
+ wait: Capybara.default_max_wait_time
166
+ }
167
+ end
168
+ end
169
+
170
+ # Instantiated eagerly so the require-time defaults above are evaluated
171
+ # NOW, at load, not at the first SnapDiff.config call.
172
+ @config = Config.new
173
+
174
+ # The single consolidated settings object -- and the single storage.
175
+ # See {SnapDiff::Config}.
176
+ def self.config
177
+ @config
178
+ end
179
+
180
+ # Installs the old mattr_accessor surface onto the legacy modules,
181
+ # delegating to the single storage above. mattr_accessor used to define
182
+ # both singleton and instance accessors (the instance ones are what
183
+ # `include Capybara::Screenshot::Diff` picks up), so both are installed.
184
+ # root keeps its historical asymmetry -- readable everywhere, writable
185
+ # only at module level (it was mattr_reader plus a custom module-level
186
+ # writer) -- with the Pathname coercion living in Config#root=.
187
+ #
188
+ # Generated here rather than in config_legacy.rb (ADR-008 step 7b) for
189
+ # the same reason legacy_shims.rb generates the legacy constants here:
190
+ # the generator is code, and the v1 trees must stay alias-only so 3.0 is
191
+ # a `git rm`. Same technique, same side of the fence.
192
+ Config::MAPPING.each do |name, (mod, mattr)|
193
+ [mod, mod.singleton_class].each do |target|
194
+ target.define_method(mattr) { SnapDiff.config.public_send(name) }
195
+ next if name == :root && target == mod
196
+
197
+ target.define_method(:"#{mattr}=") { |value| SnapDiff.config.public_send(:"#{name}=", value) }
76
198
  end
77
199
  end
78
200
  end
@@ -5,12 +5,16 @@ module SnapDiff
5
5
  #
6
6
  # Internal until the v2 namespace transition; not a public contract.
7
7
  #
8
- # Warn-once-per-subject deprecation helper. Dormant: nothing in the
9
- # current codebase calls this yet -- it exists so the gated v2 shim layer
10
- # (ADR-004's +const_missing+-based legacy constant shim and the
11
- # mattr_accessor-to-Config method shims) has pre-tested warning
12
- # machinery to call into once it lands.
8
+ # Warn-once-per-subject deprecation engine for the legacy-namespace
9
+ # shims: snap_diff/legacy_shims routes every +const_missing+ hit on an
10
+ # old +Capybara::Screenshot::Diff+ / +CapybaraScreenshotDiff+ constant
11
+ # through {.warn}, so each deprecated name warns exactly once per
12
+ # process (ADR-004's v2 namespace transition).
13
13
  module Deprecation
14
+ # Everything under lib/ is "the gem"; the first caller frame outside
15
+ # it is the user code that referenced the deprecated name (same
16
+ # filtering idea as BacktraceFilter in error_with_filtered_backtrace).
17
+ GEM_LIB_DIR = File.expand_path("..", __dir__) + File::SEPARATOR
14
18
  # Emission channel: Kernel#warn, not a direct +$stderr.puts+.
15
19
  #
16
20
  # Kernel#warn delegates to +Warning.warn+ (Ruby >= 2.4), so anything
@@ -40,7 +44,7 @@ module SnapDiff
40
44
  end
41
45
  return unless first_time
42
46
 
43
- Kernel.warn(message_for(subject, replacement, category))
47
+ Kernel.warn(message_for(subject, replacement, category, caller_locations(1)))
44
48
  end
45
49
 
46
50
  # @api private
@@ -55,9 +59,23 @@ module SnapDiff
55
59
 
56
60
  private
57
61
 
58
- def message_for(subject, replacement, category)
59
- "[snap_diff deprecation] `#{subject}` is deprecated (#{category}); " \
62
+ def message_for(subject, replacement, category, locations)
63
+ message = "[snap_diff deprecation] `#{subject}` is deprecated (#{category}); " \
60
64
  "use `#{replacement}` instead."
65
+ origin = origin_for(locations)
66
+ origin ? "#{message} (called from #{origin})" : message
67
+ end
68
+
69
+ # First frame outside the gem's lib dir, formatted "file:line";
70
+ # nil when every frame is internal (or paths are unavailable).
71
+ def origin_for(locations)
72
+ (locations || []).each do |location|
73
+ path = location.absolute_path || location.path
74
+ next if path.nil? || path.start_with?(GEM_LIB_DIR)
75
+
76
+ return "#{path}:#{location.lineno}"
77
+ end
78
+ nil
61
79
  end
62
80
  end
63
81
  end
@@ -10,5 +10,25 @@ module SnapDiff
10
10
 
11
11
  Utils.find_driver_class_for(driver_option).new
12
12
  end
13
+
14
+ # Canonical driver-class cache (ADR-008 step 5b, ex
15
+ # Capybara::Screenshot::Diff::LOADED_DRIVERS): driver name => driver
16
+ # class, filled lazily by Utils.find_driver_class_for. Mutated in
17
+ # place -- including by user registration through the legacy constant,
18
+ # which legacy_shims pins as an EAGER same-object alias of this hash
19
+ # (a lazy copy would silently drop such registrations).
20
+ def self.loaded
21
+ @loaded ||= {}
22
+ end
23
+
24
+ # Canonical read API for the detected-drivers list. The value itself
25
+ # stays on Capybara::Screenshot::Diff::AVAILABLE_DRIVERS (assigned in
26
+ # config_legacy.rb at load time, exactly when detection historically
27
+ # ran); this reads it live rather than caching, because that constant
28
+ # is the published stubbing point (image_compare_test stubs it to []
29
+ # to exercise the no-drivers error path).
30
+ def self.available
31
+ Capybara::Screenshot::Diff::AVAILABLE_DRIVERS
32
+ end
13
33
  end
14
34
  end