capybara-screenshot-diff 1.15.1 → 2.0.0.beta1

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +72 -0
  3. data/docs/UPGRADING.md +227 -0
  4. data/docs/architecture.md +64 -42
  5. data/docs/thread_safety.md +15 -0
  6. data/lib/capybara/screenshot/diff/annotation_service.rb +5 -77
  7. data/lib/capybara/screenshot/diff/area_calculator.rb +5 -54
  8. data/lib/capybara/screenshot/diff/browser_helpers.rb +5 -119
  9. data/lib/capybara/screenshot/diff/config_legacy.rb +113 -0
  10. data/lib/capybara/screenshot/diff/cucumber.rb +1 -1
  11. data/lib/capybara/screenshot/diff/difference.rb +6 -102
  12. data/lib/capybara/screenshot/diff/drivers/base_driver.rb +7 -39
  13. data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +5 -299
  14. data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +5 -169
  15. data/lib/capybara/screenshot/diff/drivers.rb +7 -14
  16. data/lib/capybara/screenshot/diff/image_compare.rb +10 -228
  17. data/lib/capybara/screenshot/diff/image_preprocessor.rb +5 -70
  18. data/lib/capybara/screenshot/diff/os.rb +8 -13
  19. data/lib/capybara/screenshot/diff/reporters/default.rb +5 -3
  20. data/lib/capybara/screenshot/diff/screenshot_matcher.rb +5 -129
  21. data/lib/capybara/screenshot/diff/screenshoter.rb +5 -127
  22. data/lib/capybara/screenshot/diff/stable_screenshoter.rb +5 -104
  23. data/lib/capybara/screenshot/diff/utils.rb +5 -40
  24. data/lib/capybara/screenshot/diff/vcs.rb +5 -37
  25. data/lib/capybara/screenshot/diff/version.rb +7 -1
  26. data/lib/capybara_screenshot_diff/attempts_reporter.rb +5 -47
  27. data/lib/capybara_screenshot_diff/cucumber.rb +5 -17
  28. data/lib/capybara_screenshot_diff/dsl.rb +7 -129
  29. data/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb +6 -30
  30. data/lib/capybara_screenshot_diff/minitest.rb +6 -56
  31. data/lib/capybara_screenshot_diff/reporters/html.rb +5 -135
  32. data/lib/capybara_screenshot_diff/rspec.rb +5 -63
  33. data/lib/capybara_screenshot_diff/screenshot_assertion.rb +39 -157
  34. data/lib/capybara_screenshot_diff/screenshot_namer.rb +5 -79
  35. data/lib/capybara_screenshot_diff/snap.rb +5 -64
  36. data/lib/capybara_screenshot_diff/snap_manager.rb +5 -82
  37. data/lib/capybara_screenshot_diff/static.rb +3 -5
  38. data/lib/capybara_screenshot_diff.rb +29 -100
  39. data/lib/snap_diff/annotation_service.rb +84 -0
  40. data/lib/snap_diff/area_calculator.rb +56 -0
  41. data/lib/snap_diff/attempts_reporter.rb +51 -0
  42. data/lib/snap_diff/browser_helpers.rb +121 -0
  43. data/lib/snap_diff/capture/viewport.rb +40 -0
  44. data/lib/snap_diff/comparison.rb +238 -0
  45. data/lib/snap_diff/comparison_result.rb +104 -0
  46. data/lib/snap_diff/config.rb +6 -2
  47. data/lib/snap_diff/deprecation.rb +84 -0
  48. data/lib/snap_diff/driver.rb +37 -0
  49. data/lib/snap_diff/drivers/chunky_png_driver.rb +298 -0
  50. data/lib/snap_diff/drivers/vips_driver.rb +171 -0
  51. data/lib/snap_diff/drivers.rb +14 -0
  52. data/lib/snap_diff/dsl.rb +143 -0
  53. data/lib/snap_diff/error_with_filtered_backtrace.rb +32 -0
  54. data/lib/snap_diff/image_preprocessor.rb +68 -0
  55. data/lib/snap_diff/integrations/cucumber.rb +22 -0
  56. data/lib/snap_diff/integrations/minitest.rb +70 -0
  57. data/lib/snap_diff/integrations/rspec.rb +68 -0
  58. data/lib/snap_diff/legacy_shims.rb +99 -0
  59. data/lib/snap_diff/os.rb +17 -0
  60. data/lib/snap_diff/reporters/html.rb +143 -0
  61. data/lib/snap_diff/reporting.rb +53 -0
  62. data/lib/snap_diff/screenshot_assertion.rb +143 -0
  63. data/lib/snap_diff/screenshot_matcher.rb +113 -0
  64. data/lib/snap_diff/screenshot_namer.rb +81 -0
  65. data/lib/snap_diff/screenshoter.rb +129 -0
  66. data/lib/snap_diff/snap.rb +66 -0
  67. data/lib/snap_diff/snap_manager.rb +125 -0
  68. data/lib/snap_diff/stable_screenshoter.rb +103 -0
  69. data/lib/snap_diff/static.rb +11 -0
  70. data/lib/snap_diff/utils.rb +38 -0
  71. data/lib/snap_diff/vcs.rb +35 -0
  72. data/lib/snap_diff/version.rb +5 -0
  73. data/lib/snap_diff.rb +22 -6
  74. metadata +36 -2
  75. /data/lib/{capybara_screenshot_diff → snap_diff}/reporters/templates/report.html.erb +0 -0
@@ -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"