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
@@ -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
data/lib/snap_diff/dsl.rb CHANGED
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  # No require of "capybara_screenshot_diff" here: every Capybara::Screenshot
4
- # / CapybaraScreenshotDiff reference below is inside a method body, resolved
5
- # lazily at call time, not at this file's own load time -- so this unit has
6
- # no eager dependency on the umbrella entry point, and requiring it here
7
- # would create a require cycle back to capybara_screenshot_diff.rb (which
8
- # requires this file's old-path forwarder).
4
+ # reference below is inside a method body, resolved lazily at call time, not
5
+ # at this file's own load time -- so this unit has no eager dependency on
6
+ # the umbrella entry point, and requiring it here would create a require
7
+ # cycle back to capybara_screenshot_diff.rb (which requires this file's
8
+ # old-path forwarder).
9
9
  # DSL includes Capybara::DSL directly below, so it needs the base gem
10
10
  # loaded regardless of what pulled this file in.
11
11
  require "capybara/dsl"
@@ -59,14 +59,14 @@ module SnapDiff
59
59
  # @option options [Numeric] :area_size_limit Maximum allowed difference area size in pixels.
60
60
  # @option options [Symbol] :driver (:auto) The image processing driver to use (:auto, :chunky_png, :vips).
61
61
  # @return [Boolean] True if the screenshot was successfully captured and processed.
62
- # @raise [CapybaraScreenshotDiff::ExpectationNotMet] If comparison fails and immediate validation is enabled.
63
- # @raise [CapybaraScreenshotDiff::UnstableImage] If the image comparison is unstable.
64
- # @raise [CapybaraScreenshotDiff::WindowSizeMismatchError] If the window size doesn't match expectations.
62
+ # @raise [SnapDiff::ExpectationNotMet] If comparison fails and immediate validation is enabled.
63
+ # @raise [SnapDiff::UnstableImage] If the image comparison is unstable.
64
+ # @raise [SnapDiff::WindowSizeMismatchError] If the window size doesn't match expectations.
65
65
  def assert_matches_screenshot(name, skip_stack_frames: 0, **options)
66
66
  return false unless Capybara::Screenshot.active?
67
67
 
68
68
  # Get the full name with section and group information
69
- full_name = CapybaraScreenshotDiff.screenshot_namer.full_name(name)
69
+ full_name = SnapDiff.session.screenshot_namer.full_name(name)
70
70
 
71
71
  # Build the screenshot assertion
72
72
  assertion = build_screenshot_assertion(full_name, options, skip_stack_frames: skip_stack_frames + 1)
@@ -77,7 +77,7 @@ module SnapDiff
77
77
  delayed = options.fetch(:delayed, Capybara::Screenshot::Diff.delayed)
78
78
 
79
79
  if delayed
80
- CapybaraScreenshotDiff.add_assertion(assertion)
80
+ SnapDiff.session.add_assertion(assertion)
81
81
  else
82
82
  assertion.validate!
83
83
  end
@@ -104,7 +104,7 @@ module SnapDiff
104
104
  def capture_screenshot(name, **options)
105
105
  return false unless Capybara::Screenshot.active?
106
106
 
107
- full_name = CapybaraScreenshotDiff.screenshot_namer.full_name(name)
107
+ full_name = SnapDiff.session.screenshot_namer.full_name(name)
108
108
  SnapDiff::ScreenshotMatcher.new(full_name, options).capture
109
109
 
110
110
  true
@@ -137,7 +137,7 @@ module SnapDiff
137
137
  end
138
138
 
139
139
  def screenshot_namer
140
- CapybaraScreenshotDiff.screenshot_namer
140
+ SnapDiff.session.screenshot_namer
141
141
  end
142
142
  end
143
143
  end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "snap_diff/error_with_filtered_backtrace"
4
+
5
+ # ADR-008 step 2: the gem's error classes live under SnapDiff. The old
6
+ # CapybaraScreenshotDiff names (capybara_screenshot_diff.rb) are EAGER
7
+ # same-object aliases of these classes -- deliberately not const_missing
8
+ # shims, because rescue clauses and defined?/const_defined? feature
9
+ # detection in adopter code must keep behaving exactly as before
10
+ # (const_defined? never triggers const_missing).
11
+ module SnapDiff
12
+ class Error < ErrorWithFilteredBacktrace; end
13
+
14
+ class ExpectationNotMet < Error; end
15
+
16
+ class UnstableImage < Error; end
17
+
18
+ class WindowSizeMismatchError < ErrorWithFilteredBacktrace; end
19
+ end
@@ -2,7 +2,8 @@
2
2
 
3
3
  # See the matching comment in integrations/minitest.rb.
4
4
  require_relative "../dsl"
5
- require "capybara_screenshot_diff/screenshot_assertion"
5
+ require "snap_diff/screenshot_assertion"
6
+ require "snap_diff/reporting"
6
7
 
7
8
  World(::SnapDiff::DSL)
8
9
 
@@ -12,11 +13,11 @@ Before do
12
13
  end
13
14
 
14
15
  After do |scenario|
15
- if !scenario.failed? && (msg = CapybaraScreenshotDiff.pending_screenshots_message)
16
+ if !scenario.failed? && (msg = SnapDiff.pending_screenshots_message)
16
17
  skip_this_scenario(msg)
17
18
  end
18
19
  ensure
19
- CapybaraScreenshotDiff.reset
20
+ SnapDiff.reset
20
21
  end
21
22
 
22
- AfterAll { CapybaraScreenshotDiff.finalize_reporters! }
23
+ AfterAll { SnapDiff::Reporting.finalize! }
@@ -2,12 +2,11 @@
2
2
 
3
3
  require "minitest"
4
4
  require_relative "../dsl"
5
- # The CapybaraScreenshotDiff.verify/.reset/.pending_screenshots_message/
6
- # .finalize_reporters! calls below need the registry singleton machinery,
7
- # which lives in the *old* capybara_screenshot_diff/screenshot_assertion.rb
8
- # file (deliberately not moved -- see that file's own comment) and is no
9
- # longer pulled in transitively by requiring dsl.rb alone.
10
- require "capybara_screenshot_diff/screenshot_assertion"
5
+ # SnapDiff.session/.reset/.pending_screenshots_message live in
6
+ # snap_diff/screenshot_assertion, SnapDiff::Reporting.finalize! in
7
+ # snap_diff/reporting -- neither is pulled in by requiring dsl.rb alone.
8
+ require "snap_diff/screenshot_assertion"
9
+ require "snap_diff/reporting"
11
10
 
12
11
  used_deprecated_entrypoint = caller.any? do |path|
13
12
  path.include?("capybara-screenshot-diff.rb") || path.include?("capybara/screenshot/diff.rb")
@@ -29,7 +28,7 @@ module SnapDiff
29
28
  self.assertions += 1
30
29
 
31
30
  super(*args, skip_stack_frames: skip_stack_frames + 1, **opts)
32
- rescue ::CapybaraScreenshotDiff::ExpectationNotMet => e
31
+ rescue ::SnapDiff::ExpectationNotMet => e
33
32
  raise ::Minitest::Assertion, e.message
34
33
  end
35
34
 
@@ -40,18 +39,18 @@ module SnapDiff
40
39
 
41
40
  def before_teardown
42
41
  super
43
- CapybaraScreenshotDiff.verify
42
+ SnapDiff.session.verify
44
43
 
45
44
  # Computed here (before teardown/reset), but the actual `skip` is
46
45
  # deferred to `after_teardown` so a real error raised by the user's
47
46
  # `teardown` isn't masked by a pending skip recorded before it ran.
48
- @capybara_screenshot_diff_pending_message = CapybaraScreenshotDiff.pending_screenshots_message
49
- rescue CapybaraScreenshotDiff::ExpectationNotMet => e
47
+ @capybara_screenshot_diff_pending_message = SnapDiff.pending_screenshots_message
48
+ rescue SnapDiff::ExpectationNotMet => e
50
49
  assertion = ::Minitest::Assertion.new(e)
51
50
  assertion.set_backtrace(e.backtrace)
52
51
  failures << assertion
53
52
  ensure
54
- CapybaraScreenshotDiff.reset
53
+ SnapDiff.reset
55
54
  end
56
55
 
57
56
  def after_teardown
@@ -67,4 +66,4 @@ module SnapDiff
67
66
  end
68
67
  end
69
68
 
70
- ::Minitest.after_run { CapybaraScreenshotDiff.finalize_reporters! } if ::Minitest.respond_to?(:after_run)
69
+ ::Minitest.after_run { SnapDiff::Reporting.finalize! } if ::Minitest.respond_to?(:after_run)
@@ -3,7 +3,8 @@
3
3
  require "rspec/core"
4
4
  require_relative "../dsl"
5
5
  # See the matching comment in integrations/minitest.rb.
6
- require "capybara_screenshot_diff/screenshot_assertion"
6
+ require "snap_diff/screenshot_assertion"
7
+ require "snap_diff/reporting"
7
8
 
8
9
  RSpec::Matchers.define :match_screenshot do |name, **options|
9
10
  description { "match screenshot '#{name}'" }
@@ -45,7 +46,7 @@ RSpec.configure do |config|
45
46
  config.append_after do |example|
46
47
  if self.class.include?(SnapDiff::DSL)
47
48
  begin
48
- CapybaraScreenshotDiff.verify
49
+ SnapDiff.session.verify
49
50
 
50
51
  # Never mask a real failure with a pending marker. Kept as
51
52
  # defense-in-depth: `append_after` observes failures from plain
@@ -53,16 +54,16 @@ RSpec.configure do |config|
53
54
  # so a user `append_after` registered after this gem still runs
54
55
  # later than us — RSpec has no "run absolutely last" construct.
55
56
  # Mitigation for such consumers: require this gem last.
56
- if example.exception.nil? && (msg = CapybaraScreenshotDiff.pending_screenshots_message)
57
+ if example.exception.nil? && (msg = SnapDiff.pending_screenshots_message)
57
58
  skip(msg)
58
59
  end
59
- rescue CapybaraScreenshotDiff::ExpectationNotMet => e
60
+ rescue SnapDiff::ExpectationNotMet => e
60
61
  raise RSpec::Expectations::ExpectationNotMetError.new(e.message).tap { |ex| ex.set_backtrace(e.backtrace) }
61
62
  ensure
62
- CapybaraScreenshotDiff.reset
63
+ SnapDiff.reset
63
64
  end
64
65
  end
65
66
  end
66
67
 
67
- config.after(:suite) { CapybaraScreenshotDiff.finalize_reporters! }
68
+ config.after(:suite) { SnapDiff::Reporting.finalize! }
68
69
  end
@@ -24,6 +24,14 @@ require "snap_diff/drivers"
24
24
  # shared SnapDiff::Drivers module (the Drivers alias is same-object by
25
25
  # contract), so const_missing can never fire for the leaf names;
26
26
  # resolving them through the old path still warns for ...::Drivers.
27
+ # - Diff::LOADED_DRIVERS: user code registers custom drivers by mutating
28
+ # this hash in place, so it must be the exact same object as the
29
+ # canonical SnapDiff::Drivers.loaded -- a lazy warn-once shim could not
30
+ # keep a mutable alias, and warning on a supported registration surface
31
+ # would be noise. Assigned eagerly below.
32
+ # - Diff::AVAILABLE_DRIVERS: stays a real constant defined by
33
+ # config_legacy.rb (detection runs at that load moment);
34
+ # SnapDiff::Drivers.available is the canonical reader.
27
35
  module SnapDiff
28
36
  # @api private
29
37
  module LegacyShims
@@ -47,6 +55,11 @@ end
47
55
  module Capybara
48
56
  module Screenshot
49
57
  module Diff
58
+ # EAGER same-object alias of the canonical driver cache (see header).
59
+ LOADED_DRIVERS = SnapDiff::Drivers.loaded
60
+
61
+ module Reporters
62
+ end
50
63
  end
51
64
  end
52
65
  end
@@ -71,9 +84,14 @@ SnapDiff::LegacyShims.install(Capybara::Screenshot::Diff, "Capybara::Screenshot:
71
84
  ScreenshotMatcher: "SnapDiff::ScreenshotMatcher",
72
85
  Drivers: "SnapDiff::Drivers",
73
86
  ImageCompare: "SnapDiff::Comparison",
87
+ Comparison: "SnapDiff::Comparison::Images",
74
88
  Difference: "SnapDiff::ComparisonResult"
75
89
  }.freeze)
76
90
 
91
+ SnapDiff::LegacyShims.install(Capybara::Screenshot::Diff::Reporters, "Capybara::Screenshot::Diff::Reporters", {
92
+ Default: "SnapDiff::Reporters::Default"
93
+ }.freeze)
94
+
77
95
  SnapDiff::LegacyShims.install(CapybaraScreenshotDiff, "CapybaraScreenshotDiff", {
78
96
  RED_RGBA: "SnapDiff::RED_RGBA",
79
97
  ORANGE_RGBA: "SnapDiff::ORANGE_RGBA",
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SnapDiff
4
+ class Region
5
+ attr_accessor :x, :y, :width, :height
6
+
7
+ def initialize(x, y, width, height)
8
+ @x, @y, @width, @height = x, y, width, height
9
+ end
10
+
11
+ def self.from_edge_coordinates(left, top, right, bottom)
12
+ return nil unless left && top && right && bottom
13
+ return nil if right < left || bottom < top
14
+
15
+ Region.new(left, top, right - left, bottom - top)
16
+ end
17
+
18
+ def to_edge_coordinates
19
+ [left, top, right, bottom]
20
+ end
21
+
22
+ def to_top_left_corner_coordinates
23
+ [x, y, width, height]
24
+ end
25
+
26
+ def top
27
+ y
28
+ end
29
+
30
+ def bottom
31
+ y + height
32
+ end
33
+
34
+ def left
35
+ x
36
+ end
37
+
38
+ def right
39
+ x + width
40
+ end
41
+
42
+ def size
43
+ return 0 if width < 0 || height < 0
44
+
45
+ result = width * height
46
+ result.zero? ? 1 : result
47
+ end
48
+
49
+ def to_a
50
+ [@x, @y, @width, @height]
51
+ end
52
+
53
+ def find_intersect_with(region)
54
+ return nil unless intersect?(region)
55
+
56
+ new_left = [x, region.x].max
57
+ new_top = [y, region.y].max
58
+
59
+ Region.new(new_left, new_top, [right, region.right].min - new_left, [bottom, region.bottom].min - new_top)
60
+ end
61
+
62
+ def intersect?(region)
63
+ left <= region.right && right >= region.left && top <= region.bottom && bottom >= region.top
64
+ end
65
+
66
+ def move_by(right_by, down_by)
67
+ Region.new(x + right_by, y + down_by, width, height)
68
+ end
69
+
70
+ def find_relative_intersect(region)
71
+ intersect = find_intersect_with(region)
72
+ return nil unless intersect
73
+
74
+ intersect.move_by(-x, -y)
75
+ end
76
+
77
+ def cover?(x, y)
78
+ x.between?(left, right) && y.between?(top, bottom)
79
+ end
80
+
81
+ def empty?
82
+ width.zero? || height.zero?
83
+ end
84
+
85
+ def blank?
86
+ empty?
87
+ end
88
+
89
+ def present?
90
+ !empty?
91
+ end
92
+
93
+ def inspect
94
+ "Region(x: #{x}, y: #{y}, width: #{width}, height: #{height})"
95
+ end
96
+
97
+ # need to add this method to make it work with assert_equal
98
+ def ==(other)
99
+ case other
100
+ when Region
101
+ x == other.x && y == other.y && width == other.width && height == other.height
102
+ when Array
103
+ to_a == other
104
+ else
105
+ false
106
+ end
107
+ end
108
+ end
109
+ end
110
+
111
+ # Compatibility alias -- EAGER and same-object on purpose, mirroring the
112
+ # ADR-008 step 2 rationale: user configs construct skip_area entries with
113
+ # top-level `Region.new(...)` and SnapDiff internals check
114
+ # `is_a?(SnapDiff::Region)`, so both names must be the exact same class;
115
+ # and `defined?(Region)` feature detection must be truthy as soon as this
116
+ # file loads (a lazy const_missing shim would return nil there).
117
+ Region = SnapDiff::Region