capybara-screenshot-diff 1.10.3 → 1.12.0

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +64 -0
  3. data/Rakefile +29 -1
  4. data/capybara-screenshot-diff.gemspec +4 -3
  5. data/docs/RELEASE_PREP.md +58 -0
  6. data/docs/UPGRADING.md +390 -0
  7. data/docs/ci-integration.md +208 -0
  8. data/docs/configuration.md +379 -0
  9. data/docs/docker-testing.md +24 -0
  10. data/docs/drivers.md +102 -0
  11. data/docs/framework-setup.md +87 -0
  12. data/docs/images/snap_diff_web_ui.png +0 -0
  13. data/docs/organization.md +226 -0
  14. data/docs/reporters.md +46 -0
  15. data/docs/thread_safety.md +97 -0
  16. data/gems.rb +2 -1
  17. data/lib/capybara/screenshot/diff/area_calculator.rb +1 -1
  18. data/lib/capybara/screenshot/diff/browser_helpers.rb +14 -1
  19. data/lib/capybara/screenshot/diff/comparison.rb +3 -0
  20. data/lib/capybara/screenshot/diff/difference.rb +40 -3
  21. data/lib/capybara/screenshot/diff/difference_finder.rb +97 -0
  22. data/lib/capybara/screenshot/diff/drivers/base_driver.rb +4 -0
  23. data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +22 -24
  24. data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +40 -27
  25. data/lib/capybara/screenshot/diff/image_compare.rb +112 -123
  26. data/lib/capybara/screenshot/diff/image_preprocessor.rb +72 -0
  27. data/lib/capybara/screenshot/diff/reporters/default.rb +10 -11
  28. data/lib/capybara/screenshot/diff/screenshot_matcher.rb +63 -36
  29. data/lib/capybara/screenshot/diff/screenshoter.rb +9 -8
  30. data/lib/capybara/screenshot/diff/stable_screenshoter.rb +7 -9
  31. data/lib/capybara/screenshot/diff/vcs.rb +19 -52
  32. data/lib/capybara/screenshot/diff/version.rb +1 -1
  33. data/lib/capybara_screenshot_diff/backtrace_filter.rb +20 -0
  34. data/lib/capybara_screenshot_diff/cucumber.rb +2 -0
  35. data/lib/capybara_screenshot_diff/dsl.rb +102 -7
  36. data/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb +15 -0
  37. data/lib/capybara_screenshot_diff/minitest.rb +4 -2
  38. data/lib/capybara_screenshot_diff/reporters/html.rb +137 -0
  39. data/lib/capybara_screenshot_diff/reporters/templates/report.html.erb +463 -0
  40. data/lib/capybara_screenshot_diff/rspec.rb +12 -2
  41. data/lib/capybara_screenshot_diff/screenshot_assertion.rb +61 -23
  42. data/lib/capybara_screenshot_diff/screenshot_namer.rb +81 -0
  43. data/lib/capybara_screenshot_diff/snap.rb +14 -3
  44. data/lib/capybara_screenshot_diff/snap_manager.rb +10 -2
  45. data/lib/capybara_screenshot_diff/static.rb +11 -0
  46. data/lib/capybara_screenshot_diff.rb +30 -5
  47. metadata +47 -8
  48. data/lib/capybara/screenshot/diff/test_methods.rb +0 -157
data/docs/drivers.md ADDED
@@ -0,0 +1,102 @@
1
+ # Image Processing Drivers
2
+
3
+ ## Perceptual color comparison (VIPS only)
4
+
5
+ By default, color differences are measured using raw RGB channel distance. This can produce
6
+ false positives from anti-aliasing and sub-pixel font rendering — the same page rendered on
7
+ different OS versions or browsers will have slightly different pixel values at text edges.
8
+
9
+ The `perceptual_threshold` option uses the CIE dE00 formula instead, which measures color
10
+ difference the way human eyes perceive it. Anti-aliasing artifacts typically score below 2.0
11
+ on the dE00 scale and are automatically ignored.
12
+
13
+ ```ruby
14
+ # Per-screenshot: ignore anti-aliasing, catch real visual changes
15
+ screenshot 'dashboard', perceptual_threshold: 2.0
16
+
17
+ # Global: apply to all screenshots
18
+ Capybara::Screenshot::Diff.perceptual_threshold = 2.0
19
+
20
+ # dE00 scale reference:
21
+ # < 1.0 — not perceptible by human eyes
22
+ # 1-2 — perceptible through close observation (anti-aliasing, font hinting)
23
+ # 2-10 — perceptible at a glance (color shifts, layout changes)
24
+ # > 10 — clearly different colors
25
+ ```
26
+
27
+ Use `perceptual_threshold` when you see false positives from font rendering differences across
28
+ CI environments, or when `color_distance_limit` with raw RGB requires frequent tuning.
29
+
30
+ **⚠️ Important:** `perceptual_threshold` and `color_distance_limit` are **mutually exclusive**.
31
+ If you set both, `perceptual_threshold` takes priority and `color_distance_limit` is silently ignored.
32
+
33
+ These options use different scales and algorithms:
34
+ - `perceptual_threshold` → CIE dE00 perceptual distance (0-100+)
35
+ - `color_distance_limit` → Euclidean RGBA distance (0-510)
36
+
37
+ **Choose one based on your driver setup:**
38
+ - VIPS with `ruby-vips` gem → prefer `perceptual_threshold`
39
+ - ChunkyPNG (no native dependencies) → use `color_distance_limit`
40
+
41
+ ## Available Image Processing Drivers
42
+
43
+ There are several image processing supported by this gem.
44
+ There are several options to setup active driver: `:auto`, `:chunky_png` and `:vips`.
45
+
46
+ * `:auto` - will try to load `:vips` if there is gem `ruby-vips`, in other cases will load `:chunky_png`
47
+ * `:chunky_png` and `:vips` will load correspondent driver
48
+
49
+ ## Enable VIPS image processing
50
+
51
+ [Vips](https://www.rubydoc.info/gems/ruby-vips/Vips/Image) driver provides a faster comparison,
52
+ and could be enabled by adding `ruby-vips` to `Gemfile`.
53
+
54
+ If need to setup explicitly Vips driver, there are several ways to do this:
55
+
56
+ * Globally: `Capybara::Screenshot::Diff.driver = :vips`
57
+ * Per screenshot option: `screenshot 'index', driver: :vips`
58
+
59
+ With enabled VIPS there are new alternatives to process differences, which are easier to find and support.
60
+ For example, `shift_distance_limit` is a very heavy operation. Instead, use `median_filter_window_size`.
61
+
62
+ ## Tolerance level (vips only)
63
+
64
+ You can set a "tolerance" anywhere from 0% to 100%. This is the amount of change that's allowable.
65
+ If the screenshot has changed by more than that amount, it'll flag it as a failure.
66
+
67
+ This is alternative to "Allowed difference size", only the difference that area calculates including valid pixels.
68
+ But "tolerance" compares only different pixels.
69
+
70
+ You can use the `tolerance` option to the `screenshot` method to set level:
71
+
72
+ ```ruby
73
+ test 'unstable area' do
74
+ visit '/'
75
+ # tolerance: 0.01 allows 1% of pixels to differ (use for noisy pages)
76
+ screenshot 'index', tolerance: 0.01
77
+ end
78
+ ```
79
+
80
+ You can also set this globally:
81
+
82
+ ```ruby
83
+ # Default for VIPS is 0.001 (0.1% pixel difference allowed)
84
+ Capybara::Screenshot::Diff.tolerance = 0.001
85
+ ```
86
+
87
+ ## Median filter size (vips only)
88
+
89
+ This is an alternative to "Allowed shift distance", but much faster.
90
+ You can find more about this strategy on [Median Filter](https://en.wikipedia.org/wiki/Median_filter).
91
+ Think about this like smoothing of the image, before comparison.
92
+
93
+ You can use the `median_filter_window_size` option to the `screenshot` method to set level:
94
+
95
+ ```ruby
96
+ test 'unstable area' do
97
+ visit '/'
98
+ screenshot 'index', median_filter_window_size: 2
99
+ end
100
+ ```
101
+
102
+ [← Back to README](../README.md)
@@ -0,0 +1,87 @@
1
+ # Framework Setup
2
+
3
+ ## Including DSL
4
+
5
+ To use the screenshot capturing and change detection features in your tests, include the `CapybaraScreenshotDiff::DSL` in your test classes. It provides the `screenshot` method to capture and compare screenshots.
6
+
7
+ There are different modules for different testing frameworks integrations.
8
+
9
+ ## Minitest
10
+
11
+ For Minitest, need to require `capybara_screenshot_diff/minitest`.
12
+ In your test class, include the `CapybaraScreenshotDiff::Minitest::Assertions` module:
13
+
14
+ ```ruby
15
+ require 'capybara_screenshot_diff/minitest'
16
+
17
+ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
18
+ # Make the Capybara & Capybara Screenshot Diff DSLs available in tests
19
+ include CapybaraScreenshotDiff::DSL
20
+ # Make `assert_*` methods behave like Minitest assertions
21
+ include CapybaraScreenshotDiff::Minitest::Assertions
22
+
23
+ def test_my_feature
24
+ visit '/'
25
+ assert_matches_screenshot 'index'
26
+ end
27
+ end
28
+ ```
29
+
30
+ ## RSpec
31
+
32
+ To use the screenshot capturing and change detection features in your tests,
33
+ include the `CapybaraScreenshotDiff::DSL` in your test classes.
34
+ It adds `match_screenshot` matcher to RSpec.
35
+
36
+ > **Important**:
37
+ > The `CapybaraScreenshotDiff::DSL` is automatically included in all feature and system tests by default.
38
+
39
+
40
+ ```ruby
41
+ require 'capybara_screenshot_diff/rspec'
42
+
43
+ describe 'Permissions admin', type: :feature do
44
+ it 'works with permissions' do
45
+ visit('/')
46
+ expect(page).to match_screenshot('home_page')
47
+ end
48
+ end
49
+
50
+
51
+ describe 'Permissions admin', type: :non_feature do
52
+ include CapybaraScreenshotDiff::DSL
53
+
54
+ it 'works with permissions' do
55
+ visit('/')
56
+ expect(page).to match_screenshot('home_page')
57
+ end
58
+ end
59
+ ```
60
+
61
+ ## Cucumber
62
+
63
+ Load Cucumber support by adding the following line (typically to your `features/support/env.rb` file):
64
+
65
+ ```ruby
66
+ require 'capybara_screenshot_diff/cucumber'
67
+ ```
68
+
69
+ And in the steps you can use:
70
+
71
+ ```ruby
72
+ Then('I should not see any visual difference') do
73
+ screenshot 'homepage'
74
+ end
75
+ ```
76
+
77
+ ## Custom Test Frameworks
78
+
79
+ Minitest, RSpec, and Cucumber are supported out of the box. For other frameworks, call `finalize_reporters!` in your framework's "after suite" hook:
80
+
81
+ ```ruby
82
+ CapybaraScreenshotDiff.finalize_reporters!
83
+ ```
84
+
85
+ This generates the HTML report and prints the summary.
86
+
87
+ [← Back to README](../README.md)
Binary file
@@ -0,0 +1,226 @@
1
+ # Screenshot Organization
2
+
3
+ ## Taking screenshots
4
+
5
+ Add `screenshot '<my_feature>'` to your tests. The screenshot will be saved in
6
+ the `doc/screenshots` directory.
7
+
8
+ Change your existing `save_screenshot` calls to `screenshot`
9
+
10
+ ```ruby
11
+ test 'my useful feature' do
12
+ visit '/'
13
+ screenshot 'welcome_index'
14
+ click_button 'Useful feature'
15
+ screenshot 'feature_index'
16
+ click_button 'Perform action'
17
+ screenshot 'action_performed'
18
+ end
19
+ ```
20
+
21
+ This will produce a sequence of images like this
22
+
23
+ ```
24
+ doc
25
+ screenshots
26
+ action_performed
27
+ feature_index
28
+ welcome_index
29
+ ```
30
+
31
+ To store the screenshot history, add the `doc/screenshots` directory to your
32
+ version control system (git).
33
+
34
+ Screenshots are compared to the previously COMMITTED version of the same screenshot.
35
+
36
+ ### Generated artifacts (do not commit)
37
+
38
+ When a screenshot differs, the gem generates temporary diff files alongside the baseline:
39
+
40
+ | Pattern | Description |
41
+ |---------|-------------|
42
+ | `*.base.png` | VCS checkout of the committed baseline |
43
+ | `*.diff.png` | Annotated diff with changes highlighted |
44
+ | `*.base.diff.png` | Annotated baseline with diff region marked |
45
+ | `*.heatmap.diff.png` | Heatmap of pixel differences |
46
+ | `snap_diff_report.html` | Interactive Web UI report |
47
+
48
+ Add these to `.gitignore`:
49
+
50
+ ```gitignore
51
+ *.diff.png
52
+ *.base.png
53
+ *.diff.webp
54
+ *.base.webp
55
+ snap_diff_report.html
56
+ ```
57
+
58
+ Clean up artifacts with `rake snap_diff:clean`.
59
+
60
+ ## Screenshot groups
61
+
62
+ Commonly it is useful to group screenshots around a feature, and record them as
63
+ a sequence. To do this, add a `screenshot_group` call to the start of your
64
+ test.
65
+
66
+ ```ruby
67
+ test 'my useful feature' do
68
+ screenshot_group 'useful_feature'
69
+ visit '/'
70
+ screenshot 'welcome_index'
71
+ click_button 'Useful feature'
72
+ screenshot 'feature_index'
73
+ click_button 'Perform action'
74
+ screenshot 'action_performed'
75
+ end
76
+ ```
77
+
78
+ This will produce a sequence of images like this
79
+
80
+ ```
81
+ doc
82
+ screenshots
83
+ useful_feature
84
+ 00_welcome_index
85
+ 01_feature_index
86
+ 02_action_performed
87
+ ```
88
+
89
+ **Note:** `screenshot_group` sets the group name for organizing screenshots. It does not delete existing files.
90
+
91
+
92
+ ## Screenshot sections
93
+
94
+ You can introduce another level above the screenshot group called a
95
+ `screenshot_section`. The section name is inserted just before the group name
96
+ in the save path. If called in the setup of the test, all screenshots in
97
+ that test will get the same prefix:
98
+
99
+ ```ruby
100
+ setup do
101
+ screenshot_section 'my_feature'
102
+ end
103
+
104
+ test 'my subfeature' do
105
+ screenshot_group 'subfeature'
106
+ visit '/feature'
107
+ click_button 'Interesting button'
108
+ screenshot 'subfeature_index'
109
+ click_button 'Perform action'
110
+ screenshot 'action_performed'
111
+ end
112
+ ```
113
+
114
+ This will produce a sequence of images like this
115
+
116
+ ```
117
+ doc
118
+ screenshots
119
+ my_feature
120
+ subfeature
121
+ 00_subfeature_index
122
+ 01_action_performed
123
+ ```
124
+
125
+
126
+ ## Setting `screenshot_section` and/or `screenshot_group` for all tests
127
+
128
+ Setting the `screenshot_section` and/or `screenshot_group` for all tests can be
129
+ done in the super class setup:
130
+
131
+ ```ruby
132
+ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
133
+ setup do
134
+ screenshot_section class_name.underscore.sub(/(_feature|_system)?_test$/, '')
135
+ screenshot_group name[5..-1]
136
+ end
137
+ end
138
+ ```
139
+
140
+ `screenshot_section` and/or `screenshot_group` can still be overridden in each
141
+ test.
142
+
143
+
144
+ ## Capturing one area instead of the whole page
145
+
146
+ You can crop images before comparison to be run, by providing region to crop as `[left, top, right, bottom]` or by css selector like `body .tag`
147
+
148
+ ```ruby
149
+ test 'the cool' do
150
+ visit '/feature'
151
+ screenshot 'cool_element', crop: '#my_element'
152
+ end
153
+ ```
154
+
155
+ **Note:** When using a retina device screenshots dimensions might be off. If
156
+ you are using (headless) chrome you can prevent this by setting the
157
+ `force-device-scale-factor` argument to `1`.
158
+
159
+ For Rails system specs using selenium you can do so for example by using the
160
+ following snippet:
161
+
162
+ ```ruby
163
+ driven_by :selenium, using: :chrome_headless do |options|
164
+ options.args << '--force-device-scale-factor=1'
165
+ end
166
+ ```
167
+
168
+ ## Multiple Capybara drivers
169
+
170
+ Often it is useful to test your app using different browsers. To avoid the
171
+ screenshots for different Capybara drivers to overwrite each other, set
172
+
173
+ ```ruby
174
+ Capybara::Screenshot.add_driver_path = true
175
+ ```
176
+
177
+ The example above will then save your screenshots like this
178
+ (for poltergeist and selenium):
179
+
180
+ ```
181
+ doc
182
+ screenshots
183
+ poltergeist
184
+ useful_feature
185
+ 00_welcome_index
186
+ 01_feature_index
187
+ 02_action_performed
188
+ selenium
189
+ useful_feature
190
+ 00_welcome_index
191
+ 01_feature_index
192
+ 02_action_performed
193
+ ```
194
+
195
+ ## Multiple OSs
196
+
197
+ If you run your tests on multiple operating systems, you will most likely find
198
+ the screen shots differ. To avoid the screenshots for different OSs to
199
+ overwrite each other, set
200
+
201
+ ```ruby
202
+ Capybara::Screenshot.add_os_path = true
203
+ ```
204
+
205
+ The example above will then save your screenshots like this
206
+ (for Linux and Windows):
207
+
208
+ ```
209
+ doc
210
+ screenshots
211
+ linux
212
+ useful_feature
213
+ 00_welcome_index
214
+ 01_feature_index
215
+ 02_action_performed
216
+ windows
217
+ useful_feature
218
+ 00_welcome_index
219
+ 01_feature_index
220
+ 02_action_performed
221
+ ```
222
+
223
+ If you combine this config with the `add_driver_path` config, the driver will be
224
+ put in front of the OS name.
225
+
226
+ [← Back to README](../README.md)
data/docs/reporters.md ADDED
@@ -0,0 +1,46 @@
1
+ # Reporters
2
+
3
+ ## Web UI for Reviewing Screenshot Changes
4
+
5
+ Generate an interactive Web UI report of screenshot differences:
6
+
7
+ ```ruby
8
+ # Add to test_helper.rb — one line, that's it
9
+ require 'capybara_screenshot_diff/reporters/html'
10
+ ```
11
+
12
+ After running tests, open the report (generated only when there are failures):
13
+
14
+ ```bash
15
+ open doc/screenshots/snap_diff_report.html
16
+ ```
17
+
18
+ The report includes a sidebar with thumbnails, side-by-side comparison with diff toggle, search, and summary stats. No configuration needed — just require it.
19
+
20
+ **Note:** The report is not generated when all screenshots match. In parallel test environments, each worker writes to the same file — the last worker's results will be in the report.
21
+
22
+ ## Custom Reporters
23
+
24
+ Build your own reporter by implementing `record` and `finalize`:
25
+
26
+ ```ruby
27
+ class MyReporter
28
+ def record(assertions)
29
+ assertions.each do |assertion|
30
+ next unless assertion.compare&.difference&.different?
31
+ # process the failure — send to Slack, write JSON, etc.
32
+ end
33
+ end
34
+
35
+ def finalize
36
+ # called once at process exit — write summary, upload report, etc.
37
+ end
38
+ end
39
+
40
+ # Register in test_helper.rb
41
+ CapybaraScreenshotDiff.reporters << MyReporter.new
42
+ ```
43
+
44
+ Reporters are notified before assertions are cleared on each test teardown. `finalize` is called via `at_exit`.
45
+
46
+ [← Back to README](../README.md)
@@ -0,0 +1,97 @@
1
+ # Thread Safety Guide for Parallel Testing
2
+
3
+ This document explains how `snap_diff` behaves under Rails parallel tests with the `:thread` strategy.
4
+
5
+ ## Overview
6
+
7
+ `snap_diff` is thread safe for parallel test execution as long as global configuration is set before tests run. Per-thread state is isolated, and shared state is protected where it matters.
8
+
9
+ ## Architecture Summary
10
+
11
+ ### Per-thread Assertion Registry
12
+
13
+ Each thread gets its own `AssertionRegistry` stored in thread-local storage:
14
+
15
+ ```ruby
16
+ def registry
17
+ Thread.current[:capybara_screenshot_diff_registry] ||= AssertionRegistry.new
18
+ end
19
+ ```
20
+
21
+ This prevents cross-thread leakage for assertions and screenshot naming.
22
+
23
+ ### Reporters Snapshot on Notify
24
+
25
+ Reporters are notified using a snapshot protected by an eagerly initialized mutex:
26
+
27
+ ```ruby
28
+ @reporters_mutex = Mutex.new
29
+
30
+ def notify_reporters(assertions)
31
+ reporters_snapshot = reporters_mutex.synchronize { reporters.dup }
32
+ reporters_snapshot.each { |reporter| reporter.record(assertions) }
33
+ end
34
+ ```
35
+
36
+ This ensures a stable list while notifying without forcing a global lock around reporter work.
37
+
38
+ ### HTML Reporter Internal Lock
39
+
40
+ The HTML reporter protects `@failures`, `@total`, and `@finalized` with a mutex so `record` and `finalize` can run safely:
41
+
42
+ ```ruby
43
+ @mutex.synchronize do
44
+ return if @finalized
45
+ @total += total
46
+ @failures.concat(failures)
47
+ end
48
+ ```
49
+
50
+ `@finalized` is set only after `write_report` succeeds, so a failed write can be retried.
51
+
52
+ ### Screenshot Naming Isolation
53
+
54
+ Each thread gets its own `ScreenshotNamer` via the per-thread registry, so counters, sections, and groups do not collide.
55
+
56
+ ### SnapManager Per Call
57
+
58
+ `SnapManager` returns a new instance for each call, avoiding shared mutable state.
59
+
60
+ ## Global Configuration
61
+
62
+ Configuration uses `mattr_accessor` and should be set once before tests run. Do not mutate config during parallel execution.
63
+
64
+ ## Parallel Test Lifecycle
65
+
66
+ - Setup: per-thread registry is created, config is read
67
+ - Execution: assertions are added to the thread-local registry
68
+ - Teardown: `verify` and `reset` operate on the thread-local registry, reporters are notified
69
+ - Exit: reporters finalize once per process (using mutex-protected snapshot)
70
+
71
+ ## Usage Examples
72
+
73
+ ```ruby
74
+ parallelize(workers: :number_of_processors, with: :threads)
75
+
76
+ Capybara::Screenshot::Diff.configure do |screenshot, diff|
77
+ screenshot.window_size = [1280, 1024]
78
+ screenshot.save_path = "doc/screenshots"
79
+ diff.tolerance = 0.001
80
+ end
81
+ ```
82
+
83
+ ## Do and Do Not
84
+
85
+ Do:
86
+ - Set config once in test helper
87
+ - Pass per-screenshot options in the call
88
+
89
+ Do not:
90
+ - Change global config inside tests
91
+ - Manually mutate registry internals
92
+
93
+ ## File System Notes
94
+
95
+ - Paths are unique per screenshot name and counter
96
+ - `FileUtils.mv` is atomic on most file systems
97
+ - Directory creation uses `mkpath`
data/gems.rb CHANGED
@@ -15,7 +15,8 @@ gem "ruby-vips", require: false
15
15
  group :test do
16
16
  gem "capybara", ">= 3.26"
17
17
  gem "mutex_m" # Needed for RubyMine debugging. Try removing it.
18
- gem "minitest", require: false
18
+ gem "minitest", "< 6", require: false
19
+ gem "minitest-mock", require: false
19
20
  gem "minitest-stub-const", require: false
20
21
  gem "simplecov", require: false
21
22
  gem "rspec", require: false
@@ -48,7 +48,7 @@ module Capybara
48
48
  def build_regions_for(coordinates)
49
49
  coordinates
50
50
  .map { |entry| Region.from_edge_coordinates(*entry) }
51
- .tap { |it| it.compact! }
51
+ .tap { |region| region.compact! }
52
52
  end
53
53
  end
54
54
  end
@@ -60,6 +60,19 @@ module Capybara
60
60
  session.execute_script(HIDE_CARET_SCRIPT)
61
61
  end
62
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
+
63
76
  FIND_ACTIVE_ELEMENT_SCRIPT = <<~JS
64
77
  function activeElement(){
65
78
  const ae = document.activeElement;
@@ -85,7 +98,7 @@ module Capybara
85
98
  JS
86
99
 
87
100
  def self.all_visible_regions_for(selector)
88
- BrowserHelpers.session.all(selector, visible: true).map(&method(:region_for))
101
+ BrowserHelpers.session.all(selector, visible: true).map { |el| region_for(el) }
89
102
  end
90
103
 
91
104
  def self.region_for(element)
@@ -2,5 +2,8 @@
2
2
 
3
3
  module Capybara::Screenshot::Diff
4
4
  class Comparison < Struct.new(:new_image, :base_image, :options, :driver, :new_image_path, :base_image_path)
5
+ def skip_area
6
+ options[:skip_area]
7
+ end
5
8
  end
6
9
  end
@@ -5,7 +5,31 @@ require "json"
5
5
  module Capybara
6
6
  module Screenshot
7
7
  module Diff
8
- class Difference < Struct.new(:region, :meta, :comparison, :failed_by)
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
+
9
33
  def different?
10
34
  failed? || !(blank? || tolerable?)
11
35
  end
@@ -27,7 +51,7 @@ module Capybara
27
51
  end
28
52
 
29
53
  def skip_area
30
- options[:skip_area]
54
+ comparison.skip_area
31
55
  end
32
56
 
33
57
  def area_size_limit
@@ -39,7 +63,7 @@ module Capybara
39
63
  end
40
64
 
41
65
  def region_area_size
42
- region&.size || 0
66
+ @region_area_size ||= region&.size || 0
43
67
  end
44
68
 
45
69
  def ratio
@@ -61,6 +85,19 @@ module Capybara
61
85
  def tolerable?
62
86
  !!((area_size_limit && area_size_limit >= region_area_size) || (tolerance && tolerance >= ratio))
63
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
64
101
  end
65
102
  end
66
103
  end