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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e22999718673e3e7cec926c524deedb3e3d0ead26e7c8350b777f5a453f02ad6
4
- data.tar.gz: 1c10e980fb7787a493c763ddc3ee8f1a7ada5180af7277e389b24761a359f68d
3
+ metadata.gz: 183664107a9aaffa1f5e89fb5c4ba3b74e798d856ed22d3256a3d3b03e528231
4
+ data.tar.gz: b2a226430eaeab196e297efc9c476eb4cf0d192334d1503ba29f330fb9bd81ed
5
5
  SHA512:
6
- metadata.gz: 6a3328839042af4d915150aba0c135d52f99074b9c31a69bdf9f6ff7e2b5a4c2c352b94f767ed9ec24f517c232257d1111bce1db3f019a668b8b4ba5314b09b1
7
- data.tar.gz: 30c9cabdbabf2823c7fe77b4b825f7d59d0e3a8b49d373544eabb3ad171bee58d9ca8e57c6555b93254e7f04d0aa04075dd84839f4623151ae818efdd020845d
6
+ metadata.gz: fda1cec9f451076ee8260b3f9d3777b1c384e6ba5ca7119a08b265cb5fec23ffe6c7315e6f97feb3c2199ab9dcf99d827eda54e3cd0857c671e270016af61af3
7
+ data.tar.gz: 2651d17eaceca6a6b8f5c94136e8e221639ec5cf70d70d9178d9d30a630f5014e5eb240ace31233a45a456cf3918d0e70b324c998e2b4ab3531bac7d7d21ffe0
data/CHANGELOG.md CHANGED
@@ -5,6 +5,65 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [v2.0.0.alpha1] - 2026-08-22
9
+
10
+ **Opt-in experiment prerelease.** RubyGems never installs prereleases by default
11
+ resolution — to try it: `gem "capybara-screenshot-diff", "2.0.0.alpha1"`. The
12
+ final 2.0.0 ships only after adopter feedback on this train; please report
13
+ anything surprising on [#166](https://github.com/snap-diff/snap_diff-capybara/issues/166).
14
+
15
+ ### Changed
16
+ - **`SnapDiff` is the canonical namespace.** The implementation lives in
17
+ `lib/snap_diff/`; every legacy constant (`Capybara::Screenshot::Diff::*`,
18
+ `CapybaraScreenshotDiff::*`) still resolves to the *same object* and keeps
19
+ working ([#208](https://github.com/snap-diff/snap_diff-capybara/pull/208),
20
+ [#209](https://github.com/snap-diff/snap_diff-capybara/pull/209),
21
+ [#210](https://github.com/snap-diff/snap_diff-capybara/pull/210)):
22
+ `ImageCompare` → `SnapDiff::Comparison`, `Difference` →
23
+ `SnapDiff::ComparisonResult`, `Drivers::BaseDriver` → `SnapDiff::Driver` (mixin).
24
+ - **Legacy constants warn on first use** — once per constant per process, naming
25
+ the replacement ([#218](https://github.com/snap-diff/snap_diff-capybara/pull/218)).
26
+ Silence with `SnapDiff.silence_deprecations = true` or
27
+ `SNAP_DIFF_SILENCE_DEPRECATIONS=1`. A few constants stay silent by design
28
+ (`Os`, `DSL`, `VERSION`, driver leaf classes — see `lib/snap_diff/legacy_shims.rb`).
29
+
30
+ ### Fixed
31
+ - Annotation color constants resolve under the bare `require "snap_diff"` entry;
32
+ previously a differing comparison raised `NameError` there
33
+ ([#219](https://github.com/snap-diff/snap_diff-capybara/pull/219))
34
+
35
+ ### Known caveats (deliberate, alpha)
36
+ - `defined?(...)` / `const_defined?` on lazily-shimmed legacy names returns
37
+ false/nil — feature detection like `defined?(Capybara::Screenshot::Diff::Drivers::VipsDriver)`
38
+ must move to the `SnapDiff::` name. Rescuing legacy error classes is unaffected
39
+ (they remain eagerly defined).
40
+ - Reopening `module Capybara::Screenshot::Diff::Drivers` (historical custom-driver
41
+ monkey-patch pattern) defines a fresh module that shadows the shim; define custom
42
+ drivers under `SnapDiff::Drivers` instead.
43
+
44
+ ### Internal
45
+ - Core simplification, behavior-preserving (the "5.5-lite" pass): pure
46
+ capture/comparison option partition ([#212](https://github.com/snap-diff/snap_diff-capybara/pull/212)),
47
+ stability failure as data ([#213](https://github.com/snap-diff/snap_diff-capybara/pull/213)),
48
+ explicit `ScreenshotAssertion#archive_baseline!` + `#inspect` on
49
+ assertion/result ([#214](https://github.com/snap-diff/snap_diff-capybara/pull/214)),
50
+ `SnapDiff::Capture::Viewport` seam with the future `anchor:` hook
51
+ ([#215](https://github.com/snap-diff/snap_diff-capybara/pull/215)), reporter vs
52
+ session lifecycle separation ([#216](https://github.com/snap-diff/snap_diff-capybara/pull/216)),
53
+ all behind a guard-test net ([#211](https://github.com/snap-diff/snap_diff-capybara/pull/211))
54
+
55
+ ---
56
+
57
+ ## [v1.15.1] - 2026-08-22
58
+
59
+ ### Fixed
60
+ - **`VipsDriver#resize_image_to` resizes to the requested dimensions** — the vips driver passed a target aspect ratio where libvips expects a scale factor, so retina-halving on macOS/Selenium could enlarge screenshots (2560×1600 → 4096×2560) and save wrongly-sized baselines; now scales width and height independently via `resize(scale, vscale:)` ([#205](https://github.com/snap-diff/snap_diff-capybara/pull/205))
61
+
62
+ ### Internal
63
+ - Driver contract tests pinning the shared ChunkyPNG/Vips driver seam (method surface, load/compare behavior, option handling) — the net that caught the resize bug ([#204](https://github.com/snap-diff/snap_diff-capybara/pull/204))
64
+
65
+ ---
66
+
8
67
  ## [v1.15.0] - 2026-08-22
9
68
 
10
69
  ### Added
data/docs/RELEASE_PREP.md CHANGED
@@ -1,23 +1,23 @@
1
- # Release Preparation — v1.15.0
1
+ # Release Preparation — v1.15.1
2
2
 
3
3
  ## Summary
4
4
 
5
- Additive `SnapDiff::Config`, two failure-masking fixes in the `pending_if_new`
6
- teardown paths, a `BacktraceFilter` boundary fix, and an internal hygiene pass.
5
+ HIGH-severity VipsDriver resize fix (retina halving could enlarge screenshots
6
+ and corrupt baselines) plus the driver contract tests that caught it.
7
7
 
8
8
  ## Release Checklist
9
9
 
10
10
  ### Pre-Release
11
11
 
12
- - [x] Update version to `1.15.0`
13
- - [x] Run tests: `bundle exec rake test:unit` (267 runs, 0 failures)
12
+ - [x] Update version to `1.15.1`
13
+ - [x] Run tests: `bundle exec rake test:unit` (376 runs, 0 failures)
14
14
  - [x] Update CHANGELOG.md
15
15
 
16
16
  ### Release (One Click)
17
17
 
18
18
  1. Push to GitHub
19
19
  2. Go to [Actions → Release](https://github.com/snap-diff/snap_diff-capybara/actions/workflows/release.yml)
20
- 3. Click **Run workflow**, enter `1.15.0`
20
+ 3. Click **Run workflow**, enter `1.15.1`
21
21
  4. Workflow will: test → tag → publish to RubyGems → create GitHub Release
22
22
 
23
23
  ### Post-Release
@@ -95,3 +95,18 @@ Do not:
95
95
  - Paths are unique per screenshot name and counter
96
96
  - `FileUtils.mv` is atomic on most file systems
97
97
  - Directory creation uses `mkpath`
98
+
99
+ ## Load-time thread safety
100
+
101
+ Runtime state is thread-local (above), but *loading* the gem is a separate
102
+ concern. The require graph is deliberately acyclic: `lib/snap_diff/*` units
103
+ depend only on the legacy-config leaf
104
+ (`capybara/screenshot/diff/config_legacy`) and specific sibling units, the
105
+ umbrella files depend on the units, and nothing requires back up the chain.
106
+
107
+ Eager mutual requires between entry points are forbidden, even guarded ones:
108
+ per-thread "loading" flags cannot serialize Ruby's process-global per-file
109
+ require locks, so two threads requiring opposite entry points first can
110
+ deadlock (lock-order inversion — observed deterministically before this
111
+ design). If two files ever need each other, extract the shared piece into a
112
+ leaf both can require instead.
@@ -1,79 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Capybara::Screenshot::Diff
4
- # Draws diff/skip-area rectangles and the heatmap overlay for a Difference,
5
- # and saves the resulting images to their `.diff.*` / `.heatmap.diff.*` paths.
6
- #
7
- # Extracted from Reporters::Default (ADR-004 PR 7) so the reporter only
8
- # builds error messages; this class owns all image annotation work.
9
- class AnnotationService
10
- attr_reader :annotated_image_path, :annotated_base_image_path, :heatmap_diff_path
11
-
12
- def initialize(difference)
13
- @difference = difference
14
-
15
- comparison = difference.comparison
16
- ext = comparison.new_image_path.extname.delete_prefix(".")
17
- screenshot_format = comparison.options[:screenshot_format] || (ext unless ext.empty?) || "png"
18
- @annotated_image_path = comparison.new_image_path.sub_ext(".diff.#{screenshot_format}")
19
- @annotated_base_image_path = comparison.base_image_path.sub_ext(".diff.#{screenshot_format}")
20
- @heatmap_diff_path = comparison.new_image_path.sub_ext(".heatmap.diff.#{screenshot_format}")
21
- end
22
-
23
- def clean_tmp_files
24
- annotated_base_image_path.unlink if annotated_base_image_path.exist?
25
- annotated_image_path.unlink if annotated_image_path.exist?
26
- heatmap_diff_path.unlink if heatmap_diff_path.exist?
27
- end
28
-
29
- def annotate_and_save_images
30
- save_annotation_for(new_image, annotated_image_path)
31
- save_annotation_for(base_image, annotated_base_image_path)
32
- save_heatmap_diff if difference.diff_mask
33
- end
34
-
35
- def save_annotation_for(image, image_path)
36
- image = annotate_difference(image, difference.region)
37
- image = annotate_skip_areas(image, difference.comparison.skip_area) if difference.comparison.skip_area
38
-
39
- save(image, image_path.to_path)
40
- end
41
-
42
- def annotate_difference(image, region)
43
- driver.draw_rectangles([image], region, CapybaraScreenshotDiff::RED_RGBA, offset: 1).first
44
- end
45
-
46
- def annotate_skip_areas(image, skip_areas)
47
- skip_areas.reduce(image) do |memo, region|
48
- driver.draw_rectangles([memo], region, CapybaraScreenshotDiff::ORANGE_RGBA).first
49
- end
50
- end
51
-
52
- def save(image, image_path)
53
- driver.save_image_to(image, image_path.to_s)
54
- end
55
-
56
- private
57
-
58
- attr_reader :difference
59
-
60
- def save_heatmap_diff
61
- merged_image = driver.merge(new_image, base_image)
62
- highlighted_mask = driver.highlight_mask(difference.diff_mask, merged_image, color: CapybaraScreenshotDiff::RED_RGBA)
63
-
64
- save(highlighted_mask, heatmap_diff_path.to_path)
65
- end
66
-
67
- def base_image
68
- difference.comparison.base_image
69
- end
70
-
71
- def new_image
72
- difference.comparison.new_image
73
- end
74
-
75
- def driver
76
- @_driver ||= difference.comparison.driver
77
- end
78
- end
79
- end
3
+ # Forwarder (ADR-004 v2 step 6): Capybara::Screenshot::Diff::AnnotationService
4
+ # now resolves lazily via snap_diff/legacy_shims' const_missing, with a
5
+ # deprecation warning pointing at SnapDiff::AnnotationService.
6
+ require "snap_diff/annotation_service"
7
+ require "snap_diff/legacy_shims"
@@ -1,56 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Capybara
4
- module Screenshot
5
- module Diff
6
- class AreaCalculator
7
- def initialize(crop_coordinates, skip_area)
8
- @crop_coordinates = crop_coordinates
9
- @skip_area = skip_area
10
- end
11
-
12
- def calculate_crop
13
- return @_calculated_crop if defined?(@_calculated_crop)
14
- return @_calculated_crop = nil unless @crop_coordinates
15
-
16
- # TODO: Move out from this class, this should be done on before screenshot and should not depend on Browser
17
- @crop_coordinates = BrowserHelpers.bounds_for_css(@crop_coordinates).first if @crop_coordinates.is_a?(String)
18
- @_calculated_crop = Region.from_edge_coordinates(*@crop_coordinates)
19
- end
20
-
21
- # Cast skip areas params into Region
22
- # and if there is crop then makes absolute coordinates to eb relative to crop top left corner
23
- def calculate_skip_area
24
- return nil unless @skip_area
25
-
26
- crop_region = calculate_crop
27
- skip_area = Array(@skip_area)
28
-
29
- css_selectors, coords_list = skip_area.compact.partition { |region| region.is_a? String }
30
- regions, coords_list = coords_list.partition { |region| region.is_a? Region }
31
-
32
- regions.concat(build_regions_for(BrowserHelpers.bounds_for_css(*css_selectors))) unless css_selectors.empty?
33
- regions.concat(build_regions_for(coords_list.flatten.each_slice(4))) unless coords_list.empty?
34
-
35
- regions.compact!
36
-
37
- if crop_region
38
- regions
39
- .map! { |region| crop_region.find_relative_intersect(region) }
40
- .filter! { |region| region&.present? }
41
- end
42
-
43
- regions
44
- end
45
-
46
- private
47
-
48
- def build_regions_for(coordinates)
49
- coordinates
50
- .map { |entry| Region.from_edge_coordinates(*entry) }
51
- .tap { |region| region.compact! }
52
- end
53
- end
54
- end
55
- end
56
- end
3
+ # Forwarder (ADR-004 v2 step 6): Capybara::Screenshot::Diff::AreaCalculator
4
+ # now resolves lazily via snap_diff/legacy_shims' const_missing, with a
5
+ # deprecation warning pointing at SnapDiff::AreaCalculator.
6
+ require "snap_diff/area_calculator"
7
+ require "snap_diff/legacy_shims"
@@ -1,121 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "region"
4
-
5
- module Capybara
6
- module Screenshot
7
- module BrowserHelpers
8
- def self.resize_window_if_needed
9
- if ::Capybara::Screenshot.respond_to?(:window_size) && ::Capybara::Screenshot.window_size
10
- resize_to(::Capybara::Screenshot.window_size)
11
- end
12
- end
13
-
14
- def self.resize_to(window_size)
15
- if session.driver.respond_to?(:resize)
16
- session.driver.resize(*window_size)
17
- elsif BrowserHelpers.selenium?
18
- session.driver.browser.manage.window.resize_to(*window_size)
19
- end
20
- end
21
-
22
- def self.selenium?
23
- current_capybara_driver_class <= Capybara::Selenium::Driver
24
- end
25
-
26
- def self.window_size_is_wrong?(expected_window_size = nil)
27
- selenium? && expected_window_size &&
28
- session.driver.browser.manage.window.size != ::Selenium::WebDriver::Dimension.new(*expected_window_size)
29
- end
30
-
31
- def self.bounds_for_css(*css_selectors)
32
- css_selectors.reduce([]) do |regions, selector|
33
- regions.concat(all_visible_regions_for(selector))
34
- end
35
- end
36
-
37
- IMAGE_WAIT_SCRIPT = <<~JS
38
- function pending_image() {
39
- const images = document.images
40
- for (var i = 0; i < images.length; i++) {
41
- if (!images[i].complete && images[i].loading !== "lazy") {
42
- return images[i].src
43
- }
44
- }
45
- return false
46
- }(window)
47
- JS
48
-
49
- HIDE_CARET_SCRIPT = <<~JS
50
- if (!document.getElementById('csdHideCaretStyle')) {
51
- let style = document.createElement('style');
52
- style.setAttribute('id', 'csdHideCaretStyle');
53
- document.head.appendChild(style);
54
- let styleSheet = style.sheet;
55
- styleSheet.insertRule("* { caret-color: transparent !important; }", 0);
56
- }
57
- JS
58
-
59
- def self.hide_caret
60
- session.execute_script(HIDE_CARET_SCRIPT)
61
- end
62
-
63
- DISABLE_ANIMATIONS_SCRIPT = <<~JS
64
- if (!document.getElementById('csdDisableAnimationsStyle')) {
65
- let style = document.createElement('style');
66
- style.setAttribute('id', 'csdDisableAnimationsStyle');
67
- style.textContent = '*, *::before, *::after { animation-duration: 0s !important; animation-delay: 0s !important; transition-duration: 0s !important; transition-delay: 0s !important; }';
68
- document.head.appendChild(style);
69
- }
70
- JS
71
-
72
- def self.disable_animations
73
- session.execute_script(DISABLE_ANIMATIONS_SCRIPT)
74
- end
75
-
76
- FIND_ACTIVE_ELEMENT_SCRIPT = <<~JS
77
- function activeElement(){
78
- const ae = document.activeElement;
79
- if (ae.nodeName === "INPUT" || ae.nodeName === "TEXTAREA") {
80
- ae.blur();
81
- return ae;
82
- }
83
- return null;
84
- }(window);
85
- JS
86
-
87
- def self.blur_from_focused_element
88
- session.evaluate_script(FIND_ACTIVE_ELEMENT_SCRIPT)
89
- end
90
-
91
- GET_BOUNDING_CLIENT_RECT_SCRIPT = <<~JS
92
- [
93
- this.getBoundingClientRect().left,
94
- this.getBoundingClientRect().top,
95
- this.getBoundingClientRect().right,
96
- this.getBoundingClientRect().bottom
97
- ]
98
- JS
99
-
100
- def self.all_visible_regions_for(selector)
101
- BrowserHelpers.session.all(selector, visible: true).map { |el| region_for(el) }
102
- end
103
-
104
- def self.region_for(element)
105
- element.evaluate_script(GET_BOUNDING_CLIENT_RECT_SCRIPT).map { |point| point.negative? ? 0 : point.ceil.to_i }
106
- end
107
-
108
- def self.session
109
- Capybara.current_session
110
- end
111
-
112
- def self.pending_image_to_load
113
- BrowserHelpers.session.evaluate_script(IMAGE_WAIT_SCRIPT)
114
- end
115
-
116
- def self.current_capybara_driver_class
117
- session.driver.class
118
- end
119
- end
120
- end
121
- end
3
+ # Forwarder (ADR-004 v2 step 6): Capybara::Screenshot::BrowserHelpers now
4
+ # resolves lazily via snap_diff/legacy_shims' const_missing, with a
5
+ # deprecation warning pointing at SnapDiff::BrowserHelpers.
6
+ require "snap_diff/browser_helpers"
7
+ require "snap_diff/legacy_shims"
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Legacy Capybara::Screenshot / Capybara::Screenshot::Diff mattr_accessor
4
+ # config module, extracted out of capybara_screenshot_diff.rb so it is a
5
+ # leaf: it can be required by both capybara_screenshot_diff.rb (the old
6
+ # umbrella entry point) and snap_diff.rb (the canonical entry point)
7
+ # without either of *those* requiring the other back.
8
+ #
9
+ # Why this file has to exist at all: several lib/snap_diff/* units
10
+ # (Screenshoter, SnapManager, Utils) are referenced here at class-body
11
+ # eval time (mattr_accessor default blocks, AVAILABLE_DRIVERS) rather
12
+ # than at call time, so they must be real, already-loaded classes before
13
+ # this module body runs. Since v2 step 6 they are pulled in and referenced
14
+ # under their canonical SnapDiff names: the old-name aliases are lazy
15
+ # const_missing shims that emit deprecation warnings, and the gem's own
16
+ # code must stay warning-free. Everything else this module references
17
+ # (Os, Comparison) is referenced only inside method bodies, resolved
18
+ # lazily at call time by whichever entry point loaded them.
19
+ require "snap_diff/screenshoter"
20
+ require "snap_diff/snap_manager"
21
+ require "snap_diff/utils"
22
+
23
+ module Capybara
24
+ module Screenshot
25
+ mattr_accessor :add_driver_path
26
+ mattr_accessor :add_os_path
27
+ mattr_accessor(:blur_active_element) { true }
28
+ mattr_accessor :enabled
29
+ mattr_accessor(:hide_caret) { true }
30
+ mattr_accessor :disable_animations
31
+ mattr_reader(:root) { (defined?(Rails) && defined?(Rails.root) && Rails.root) || Pathname(".").expand_path }
32
+ mattr_accessor :stability_time_limit
33
+ mattr_accessor :window_size
34
+ mattr_accessor(:save_path) { "doc/screenshots" }
35
+ mattr_accessor(:use_lfs)
36
+ mattr_accessor(:screenshot_format) { "png" }
37
+ mattr_accessor(:capybara_screenshot_options) { {} }
38
+
39
+ class << self
40
+ def root=(path)
41
+ @@root = Pathname(path).expand_path
42
+ end
43
+
44
+ def active?
45
+ enabled || (enabled.nil? && Diff.enabled)
46
+ end
47
+
48
+ def screenshot_area
49
+ parts = [Screenshot.save_path]
50
+ parts << SnapDiff::Os.name if Screenshot.add_os_path
51
+ parts << Capybara.current_driver.to_s if Screenshot.add_driver_path
52
+ File.join(*parts)
53
+ end
54
+
55
+ def screenshot_area_abs
56
+ root / screenshot_area
57
+ end
58
+ end
59
+
60
+ # Module to track screenshot changes
61
+ module Diff
62
+ mattr_accessor(:delayed) { true }
63
+ mattr_accessor :area_size_limit
64
+ mattr_accessor(:fail_if_new) { !ENV["CI"].nil? && !ENV["CI"].empty? }
65
+ mattr_accessor(:pending_if_new) { false }
66
+ mattr_accessor(:fail_on_difference) { true }
67
+ mattr_accessor :color_distance_limit
68
+ mattr_accessor(:enabled) { true }
69
+ mattr_accessor :shift_distance_limit
70
+ mattr_accessor :skip_area
71
+ mattr_accessor(:driver) { :auto }
72
+ mattr_accessor :tolerance
73
+ mattr_accessor :perceptual_threshold
74
+
75
+ mattr_accessor(:screenshoter) { SnapDiff::Screenshoter }
76
+ mattr_accessor(:manager) { SnapDiff::SnapManager }
77
+
78
+ AVAILABLE_DRIVERS = SnapDiff::Utils.detect_available_drivers.freeze
79
+
80
+ # Configure screenshot and diff settings in one block.
81
+ #
82
+ # Capybara::Screenshot::Diff.configure do |screenshot, diff|
83
+ # screenshot.window_size = [1280, 1024]
84
+ # screenshot.stability_time_limit = 1
85
+ # diff.driver = :vips
86
+ # diff.tolerance = 0.0005
87
+ # end
88
+ def self.configure
89
+ yield Screenshot, self
90
+ end
91
+
92
+ def self.compare(baseline_path, current_path, **options)
93
+ SnapDiff::Comparison.new(current_path, baseline_path, default_options.merge(options))
94
+ end
95
+
96
+ def self.default_options
97
+ {
98
+ area_size_limit: area_size_limit,
99
+ color_distance_limit: color_distance_limit,
100
+ driver: driver,
101
+ screenshot_format: Screenshot.screenshot_format,
102
+ capybara_screenshot_options: Screenshot.capybara_screenshot_options,
103
+ perceptual_threshold: perceptual_threshold,
104
+ shift_distance_limit: shift_distance_limit,
105
+ skip_area: skip_area,
106
+ stability_time_limit: Screenshot.stability_time_limit,
107
+ tolerance: tolerance || ((driver == :vips) ? 0.001 : nil),
108
+ wait: Capybara.default_max_wait_time
109
+ }
110
+ end
111
+ end
112
+ end
113
+ end
@@ -1,3 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "capybara_screenshot_diff/cucumber"
3
+ require "snap_diff/integrations/cucumber"
@@ -1,104 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "json"
4
-
5
- module Capybara
6
- module Screenshot
7
- module Diff
8
- # Represents a difference between two images
9
- #
10
- # This value object encapsulates the result of an image comparison operation.
11
- # It follows the Single Responsibility Principle by focusing solely on representing
12
- # the difference state, including:
13
- # - Whether images are different or equal
14
- # - Why they differ (dimensions, pixels, etc.)
15
- # - The specific region of difference
16
- # - Whether differences are tolerable based on configured thresholds
17
- #
18
- # As part of the layered comparison architecture, this class represents the final
19
- # output of the comparison process, containing all data needed for reporting.
20
- # Represents a difference between two images
21
- class Difference < Struct.new(:region, :meta, :comparison, :failed_by, :base_image_path, :image_path, keyword_init: nil)
22
- def self.build_null(comparison, base_image_path, new_image_path, failed_by = nil)
23
- Difference.new(
24
- nil,
25
- {difference_level: nil, max_color_distance: 0},
26
- comparison,
27
- failed_by,
28
- base_image_path,
29
- new_image_path
30
- ).freeze
31
- end
32
-
33
- def different?
34
- failed? || !(blank? || tolerable?)
35
- end
36
-
37
- def equal?
38
- !different?
39
- end
40
-
41
- def failed?
42
- !!failed_by
43
- end
44
-
45
- def options
46
- comparison.options
47
- end
48
-
49
- def tolerance
50
- options[:tolerance]
51
- end
52
-
53
- def skip_area
54
- comparison.skip_area
55
- end
56
-
57
- def area_size_limit
58
- options[:area_size_limit]
59
- end
60
-
61
- def blank?
62
- region.nil? || region_area_size.zero?
63
- end
64
-
65
- def region_area_size
66
- @region_area_size ||= region&.size || 0
67
- end
68
-
69
- def ratio
70
- meta[:difference_level]
71
- end
72
-
73
- def to_h
74
- {area_size: region_area_size, region: coordinates}.merge!(meta)
75
- end
76
-
77
- def coordinates
78
- region&.to_edge_coordinates
79
- end
80
-
81
- def inspect
82
- to_h.to_json
83
- end
84
-
85
- def tolerable?
86
- !!((area_size_limit && area_size_limit >= region_area_size) || (tolerance && tolerance >= ratio))
87
- end
88
-
89
- # Path accessors for backward compatibility
90
- def new_image_path
91
- image_path || comparison&.new_image_path
92
- end
93
-
94
- def original_image_path
95
- base_image_path || comparison&.base_image_path
96
- end
97
-
98
- def diff_mask
99
- meta[:diff_mask]
100
- end
101
- end
102
- end
103
- end
104
- end
3
+ # Forwarder (ADR-004 v2 step 6): the comparison-result value object lives at
4
+ # SnapDiff::ComparisonResult (ex-Difference); the old name now resolves
5
+ # lazily via snap_diff/legacy_shims' const_missing, with a deprecation
6
+ # warning.
7
+ require "snap_diff/comparison_result"
8
+ require "snap_diff/legacy_shims"
@@ -1,41 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "capybara/screenshot/diff/difference"
4
-
5
- module Capybara
6
- module Screenshot
7
- module Diff
8
- # Compare two images and determine if they are equal, different, or within some comparison
9
- # range considering color values and difference area size.
10
- module Drivers
11
- class BaseDriver
12
- PNG_EXTENSION = ".png"
13
-
14
- def same_dimension?(comparison)
15
- dimension(comparison.base_image) == dimension(comparison.new_image)
16
- end
17
-
18
- def height_for(image)
19
- image.height
20
- end
21
-
22
- def width_for(image)
23
- image.width
24
- end
25
-
26
- def image_area_size(image)
27
- width_for(image) * height_for(image)
28
- end
29
-
30
- def dimension(image)
31
- [width_for(image), height_for(image)]
32
- end
33
-
34
- def supports?(feature)
35
- respond_to?(feature)
36
- end
37
- end
38
- end
39
- end
40
- end
41
- end
3
+ # BaseDriver was dissolved into the SnapDiff::Driver mixin (ADR-004 v2
4
+ # step 4); since step 6 the old name resolves lazily via
5
+ # snap_diff/legacy_shims' const_missing, with a deprecation warning.
6
+ # Note it is now a module -- `class MyDriver < BaseDriver` becomes
7
+ # `include SnapDiff::Driver`.
8
+ require "snap_diff/driver"
9
+ require "snap_diff/legacy_shims"