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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +64 -0
- data/Rakefile +29 -1
- data/capybara-screenshot-diff.gemspec +4 -3
- data/docs/RELEASE_PREP.md +58 -0
- data/docs/UPGRADING.md +390 -0
- data/docs/ci-integration.md +208 -0
- data/docs/configuration.md +379 -0
- data/docs/docker-testing.md +24 -0
- data/docs/drivers.md +102 -0
- data/docs/framework-setup.md +87 -0
- data/docs/images/snap_diff_web_ui.png +0 -0
- data/docs/organization.md +226 -0
- data/docs/reporters.md +46 -0
- data/docs/thread_safety.md +97 -0
- data/gems.rb +2 -1
- data/lib/capybara/screenshot/diff/area_calculator.rb +1 -1
- data/lib/capybara/screenshot/diff/browser_helpers.rb +14 -1
- data/lib/capybara/screenshot/diff/comparison.rb +3 -0
- data/lib/capybara/screenshot/diff/difference.rb +40 -3
- data/lib/capybara/screenshot/diff/difference_finder.rb +97 -0
- data/lib/capybara/screenshot/diff/drivers/base_driver.rb +4 -0
- data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +22 -24
- data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +40 -27
- data/lib/capybara/screenshot/diff/image_compare.rb +112 -123
- data/lib/capybara/screenshot/diff/image_preprocessor.rb +72 -0
- data/lib/capybara/screenshot/diff/reporters/default.rb +10 -11
- data/lib/capybara/screenshot/diff/screenshot_matcher.rb +63 -36
- data/lib/capybara/screenshot/diff/screenshoter.rb +9 -8
- data/lib/capybara/screenshot/diff/stable_screenshoter.rb +7 -9
- data/lib/capybara/screenshot/diff/vcs.rb +19 -52
- data/lib/capybara/screenshot/diff/version.rb +1 -1
- data/lib/capybara_screenshot_diff/backtrace_filter.rb +20 -0
- data/lib/capybara_screenshot_diff/cucumber.rb +2 -0
- data/lib/capybara_screenshot_diff/dsl.rb +102 -7
- data/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb +15 -0
- data/lib/capybara_screenshot_diff/minitest.rb +4 -2
- data/lib/capybara_screenshot_diff/reporters/html.rb +137 -0
- data/lib/capybara_screenshot_diff/reporters/templates/report.html.erb +463 -0
- data/lib/capybara_screenshot_diff/rspec.rb +12 -2
- data/lib/capybara_screenshot_diff/screenshot_assertion.rb +61 -23
- data/lib/capybara_screenshot_diff/screenshot_namer.rb +81 -0
- data/lib/capybara_screenshot_diff/snap.rb +14 -3
- data/lib/capybara_screenshot_diff/snap_manager.rb +10 -2
- data/lib/capybara_screenshot_diff/static.rb +11 -0
- data/lib/capybara_screenshot_diff.rb +30 -5
- metadata +47 -8
- 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
|
|
@@ -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(
|
|
101
|
+
BrowserHelpers.session.all(selector, visible: true).map { |el| region_for(el) }
|
|
89
102
|
end
|
|
90
103
|
|
|
91
104
|
def self.region_for(element)
|
|
@@ -5,7 +5,31 @@ require "json"
|
|
|
5
5
|
module Capybara
|
|
6
6
|
module Screenshot
|
|
7
7
|
module Diff
|
|
8
|
-
|
|
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
|
-
|
|
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
|