snap_diff-capybara 0.0.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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +190 -0
  3. data/CODE_OF_CONDUCT.md +129 -0
  4. data/LICENSE.txt +21 -0
  5. data/Rakefile +65 -0
  6. data/capybara-screenshot-diff.gemspec +29 -0
  7. data/docs/RELEASE_PREP.md +44 -0
  8. data/docs/UPGRADING.md +702 -0
  9. data/docs/architecture.md +289 -0
  10. data/docs/ci-integration.md +238 -0
  11. data/docs/configuration.md +395 -0
  12. data/docs/docker-testing.md +24 -0
  13. data/docs/drivers.md +102 -0
  14. data/docs/framework-setup.md +87 -0
  15. data/docs/images/snap_diff_annotated.png +0 -0
  16. data/docs/images/snap_diff_web_ui.png +0 -0
  17. data/docs/migration-guide.md +286 -0
  18. data/docs/organization.md +204 -0
  19. data/docs/reporters.md +46 -0
  20. data/docs/thread_safety.md +112 -0
  21. data/gems.rb +39 -0
  22. data/lib/capybara/screenshot/diff/annotation_service.rb +7 -0
  23. data/lib/capybara/screenshot/diff/area_calculator.rb +7 -0
  24. data/lib/capybara/screenshot/diff/browser_helpers.rb +7 -0
  25. data/lib/capybara/screenshot/diff/config_legacy.rb +113 -0
  26. data/lib/capybara/screenshot/diff/cucumber.rb +3 -0
  27. data/lib/capybara/screenshot/diff/difference.rb +8 -0
  28. data/lib/capybara/screenshot/diff/drivers/base_driver.rb +9 -0
  29. data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +7 -0
  30. data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +7 -0
  31. data/lib/capybara/screenshot/diff/drivers.rb +9 -0
  32. data/lib/capybara/screenshot/diff/image_compare.rb +12 -0
  33. data/lib/capybara/screenshot/diff/image_preprocessor.rb +7 -0
  34. data/lib/capybara/screenshot/diff/os.rb +14 -0
  35. data/lib/capybara/screenshot/diff/region.rb +107 -0
  36. data/lib/capybara/screenshot/diff/reporters/default.rb +109 -0
  37. data/lib/capybara/screenshot/diff/screenshot_matcher.rb +7 -0
  38. data/lib/capybara/screenshot/diff/screenshoter.rb +7 -0
  39. data/lib/capybara/screenshot/diff/stable_screenshoter.rb +7 -0
  40. data/lib/capybara/screenshot/diff/utils.rb +7 -0
  41. data/lib/capybara/screenshot/diff/vcs.rb +7 -0
  42. data/lib/capybara/screenshot/diff/version.rb +15 -0
  43. data/lib/capybara/screenshot/diff.rb +3 -0
  44. data/lib/capybara-screenshot-diff.rb +3 -0
  45. data/lib/capybara_screenshot_diff/attempts_reporter.rb +7 -0
  46. data/lib/capybara_screenshot_diff/cucumber.rb +8 -0
  47. data/lib/capybara_screenshot_diff/dsl.rb +12 -0
  48. data/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb +8 -0
  49. data/lib/capybara_screenshot_diff/minitest.rb +14 -0
  50. data/lib/capybara_screenshot_diff/reporters/html.rb +7 -0
  51. data/lib/capybara_screenshot_diff/rspec.rb +8 -0
  52. data/lib/capybara_screenshot_diff/screenshot_assertion.rb +71 -0
  53. data/lib/capybara_screenshot_diff/screenshot_namer.rb +7 -0
  54. data/lib/capybara_screenshot_diff/snap.rb +7 -0
  55. data/lib/capybara_screenshot_diff/snap_manager.rb +7 -0
  56. data/lib/capybara_screenshot_diff/static.rb +9 -0
  57. data/lib/capybara_screenshot_diff.rb +50 -0
  58. data/lib/snap_diff/annotation_service.rb +84 -0
  59. data/lib/snap_diff/area_calculator.rb +56 -0
  60. data/lib/snap_diff/attempts_reporter.rb +51 -0
  61. data/lib/snap_diff/browser_helpers.rb +121 -0
  62. data/lib/snap_diff/capture/viewport.rb +40 -0
  63. data/lib/snap_diff/comparison.rb +238 -0
  64. data/lib/snap_diff/comparison_result.rb +104 -0
  65. data/lib/snap_diff/config.rb +78 -0
  66. data/lib/snap_diff/deprecation.rb +84 -0
  67. data/lib/snap_diff/driver.rb +37 -0
  68. data/lib/snap_diff/drivers/chunky_png_driver.rb +298 -0
  69. data/lib/snap_diff/drivers/vips_driver.rb +171 -0
  70. data/lib/snap_diff/drivers.rb +14 -0
  71. data/lib/snap_diff/dsl.rb +143 -0
  72. data/lib/snap_diff/error_with_filtered_backtrace.rb +32 -0
  73. data/lib/snap_diff/image_preprocessor.rb +68 -0
  74. data/lib/snap_diff/integrations/cucumber.rb +22 -0
  75. data/lib/snap_diff/integrations/minitest.rb +70 -0
  76. data/lib/snap_diff/integrations/rspec.rb +68 -0
  77. data/lib/snap_diff/legacy_shims.rb +99 -0
  78. data/lib/snap_diff/os.rb +17 -0
  79. data/lib/snap_diff/reporters/html.rb +143 -0
  80. data/lib/snap_diff/reporters/templates/report.html.erb +463 -0
  81. data/lib/snap_diff/reporting.rb +53 -0
  82. data/lib/snap_diff/screenshot_assertion.rb +143 -0
  83. data/lib/snap_diff/screenshot_matcher.rb +113 -0
  84. data/lib/snap_diff/screenshot_namer.rb +81 -0
  85. data/lib/snap_diff/screenshoter.rb +129 -0
  86. data/lib/snap_diff/snap.rb +66 -0
  87. data/lib/snap_diff/snap_manager.rb +125 -0
  88. data/lib/snap_diff/stable_screenshoter.rb +103 -0
  89. data/lib/snap_diff/static.rb +11 -0
  90. data/lib/snap_diff/utils.rb +38 -0
  91. data/lib/snap_diff/vcs.rb +35 -0
  92. data/lib/snap_diff/version.rb +5 -0
  93. data/lib/snap_diff.rb +65 -0
  94. metadata +160 -10
  95. data/README.md +0 -3
@@ -0,0 +1,286 @@
1
+ # Migration Guide
2
+
3
+ Migrate your visual regression testing from other tools to `capybara-screenshot-diff`. This guide covers the most common migration paths.
4
+
5
+ ## Why Switch?
6
+
7
+ | Factor | Percy / Chromatic | BackstopJS | `capybara-screenshot-diff` |
8
+ |--------|-------------------|------------|--------------------------|
9
+ | Pricing | Paid SaaS (snapshot limits) | Free | Free (MIT) |
10
+ | Infrastructure | Cloud service, API tokens | Node + Puppeteer | Ruby gem, no external services |
11
+ | Baselines | Hosted on their servers | Local files | Git (committed to repo) |
12
+ | Review | Web dashboard | HTML report | HTML report + GitHub PR comments |
13
+ | PR integration | GitHub app | Manual CI steps | Reusable GitHub Action |
14
+ | Offline | ❌ Requires internet | ✅ | ✅ |
15
+ | Diff in PRs | Screenshot in comment | Manual | Upload artifact + PR comment |
16
+
17
+ ## From Percy
18
+
19
+ ### Setup changes
20
+
21
+ **Before (Percy):**
22
+ ```ruby
23
+ # Gemfile
24
+ gem 'percy-capybara'
25
+
26
+ # test helper
27
+ require 'percy/capybara'
28
+
29
+ # test
30
+ def test_homepage
31
+ visit '/'
32
+ Percy::Capybara.screenshot('homepage')
33
+ end
34
+ ```
35
+
36
+ **After (capybara-screenshot-diff):**
37
+ ```ruby
38
+ # Gemfile
39
+ gem 'capybara-screenshot-diff'
40
+
41
+ # test helper
42
+ require 'capybara_screenshot_diff/minitest'
43
+
44
+ # test class
45
+ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
46
+ include CapybaraScreenshotDiff::Minitest::Assertions
47
+
48
+ test "homepage" do
49
+ visit '/'
50
+ screenshot 'homepage'
51
+ end
52
+ end
53
+ ```
54
+
55
+ ### What changes
56
+
57
+ | Concept | Percy | capybara-screenshot-diff |
58
+ |---------|-------|-------------------------|
59
+ | Baseline storage | Percy cloud | Committed to git (`doc/screenshots/`) |
60
+ | First run | Uploads to Percy | Saves locally, passes automatically |
61
+ | CI setup | `PERCY_TOKEN` env var | GitHub Action (3 lines) |
62
+ | Diff review | Percy dashboard | `snap_diff_report.html` or PR artifacts |
63
+ | Update baselines | Percy's "Approve" button | Delete file, re-run tests, commit |
64
+ | Snapshot limits | Paid plan dependent | Unlimited |
65
+ | Parallel builds | Built-in | Thread-safe with t-locals + mutex |
66
+
67
+ ### CI migration
68
+
69
+ **Before (Percy GitHub Action):**
70
+ ```yaml
71
+ - name: Percy Test
72
+ run: PERCY_TOKEN=${{ secrets.PERCY_TOKEN }} bundle exec rake test
73
+ ```
74
+
75
+ **After (capybara-screenshot-diff):**
76
+ ```yaml
77
+ - uses: snap-diff/snap_diff-capybara/.github/actions/setup-ruby-and-dependencies@master
78
+ with:
79
+ ruby-version: '4.0'
80
+ - run: bundle exec rake test
81
+ - uses: snap-diff/snap_diff-capybara/.github/actions/upload-screenshots@master
82
+ if: failure()
83
+ with:
84
+ name: screenshots
85
+ pr-comment: 'true'
86
+ ```
87
+
88
+ ### Migration steps
89
+
90
+ 1. **Remove Percy gem and configuration**
91
+ 2. **Add `capybara-screenshot-diff`** to your Gemfile
92
+ 3. **Replace `Percy::Capybara.screenshot` calls** with `screenshot` (or `match_screenshot` for RSpec)
93
+ 4. **Run tests once** to generate baselines
94
+ 5. **Commit baselines** (`git add doc/screenshots/`)
95
+ 6. **Set up CI** with the GitHub Actions upload step
96
+ 7. **Remove Percy integration** from CI
97
+
98
+ ---
99
+
100
+ ## From Chromatic
101
+
102
+ ### Setup changes
103
+
104
+ **Before (Chromatic + Storybook):**
105
+ ```js
106
+ // .storybook/preview.js
107
+ import { withScreenshot } from 'chromatic';
108
+
109
+ export const decorators = [withScreenshot];
110
+ ```
111
+
112
+ **After (capybara-screenshot-diff):**
113
+ ```ruby
114
+ # test/system/stories_test.rb
115
+ class StoriesTest < ApplicationSystemTestCase
116
+ test "landing page story" do
117
+ visit '/iframe.html?id=pages-landing--default'
118
+ screenshot 'stories/landing-page'
119
+ end
120
+ end
121
+ ```
122
+
123
+ ### Key differences
124
+
125
+ | Concept | Chromatic | capybara-screenshot-diff |
126
+ |---------|-----------|-------------------------|
127
+ | Focus | Storybook components | Full-page system tests |
128
+ | Baseline | Chromatic cloud | Git-committed |
129
+ | Review | Chromatic web UI | HTML report + PR artifacts |
130
+ | CI integration | Chromatic GitHub App | GitHub Actions + PR comments |
131
+ | Thresholds | Visual catch (AI) | Configurable tolerance (numeric) |
132
+
133
+ ### Migration approach
134
+
135
+ Chromatic is primarily for Storybook component testing. If you want to continue testing individual components:
136
+
137
+ 1. **Replace with Capybara system tests** that visit each component's rendered page
138
+ 2. **Use `crop:` option** to isolate specific elements: `screenshot 'button', crop: '.my-button'`
139
+ 3. **Use `skip_area:` option** to ignore dynamic regions: `screenshot 'dashboard', skip_area: ['.timestamp']`
140
+
141
+ ---
142
+
143
+ ## From BackstopJS
144
+
145
+ ### Setup changes
146
+
147
+ **Before (BackstopJS):**
148
+ ```json
149
+ // backstop.json
150
+ {
151
+ "id": "homepage",
152
+ "viewports": [{"width": 1280, "height": 1024}],
153
+ "scenarios": [{
154
+ "label": "Homepage",
155
+ "url": "http://localhost:3000",
156
+ "referenceUrl": "http://localhost:3000",
157
+ "selectors": ["document"]
158
+ }],
159
+ "paths": {
160
+ "bitmaps_reference": "backstop_data/bitmaps_reference",
161
+ "bitmaps_test": "backstop_data/bitmaps_test",
162
+ "html_report": "backstop_data/html_report"
163
+ }
164
+ }
165
+ ```
166
+
167
+ **After (capybara-screenshot-diff):**
168
+ ```ruby
169
+ class HomepageTest < ApplicationSystemTestCase
170
+ test "homepage" do
171
+ visit '/'
172
+ screenshot 'homepage'
173
+ end
174
+ end
175
+ ```
176
+
177
+ ### Key differences
178
+
179
+ | Concept | BackstopJS | capybara-screenshot-diff |
180
+ |---------|-----------|-------------------------|
181
+ | Language | JavaScript + Node | Ruby (runs in test suite) |
182
+ | Dependencies | Node, Puppeteer/Chromium | Ruby gems + optional libvips |
183
+ | Test runner | Standalone CLI | Minitest, RSpec, Cucumber |
184
+ | Selectors | CSS selectors for scenarios | CSS selectors for crop/skip_area |
185
+ | Viewports | Per-scenario config | Global `window_size` setting |
186
+ | CI report | HTML report | HTML report + GitHub Actions |
187
+ | Stability | `misMatchThreshold` + `delay` | `tolerance` + `stability_time_limit` |
188
+
189
+ ### Configuration mapping
190
+
191
+ | BackstopJS option | capybara-screenshot-diff equivalent |
192
+ |-------------------|-------------------------------------|
193
+ | `misMatchThreshold` | `tolerance` (0.0-1.0 scale, e.g. `0.01` = 1%) |
194
+ | `delay` | `stability_time_limit` (seconds) |
195
+ | `selectors` | `crop:` option with CSS selector |
196
+ | `hideSelectors` | `skip_area:` option with CSS selectors |
197
+ | `removeSelectors` | N/A — use `skip_area` or modify DOM before screenshot |
198
+ | `waitTimeout` | `wait:` option (defaults to `Capybara.default_max_wait_time`) |
199
+ | `viewports` | `window_size: [width, height]` |
200
+ | `onReadyScript` | Custom setup in your test's `setup` block |
201
+
202
+ ### CI migration
203
+
204
+ **Before (BackstopJS in CI):**
205
+ ```yaml
206
+ - run: npx backstop test --config=backstop.json
207
+ ```
208
+
209
+ **After (capybara-screenshot-diff in CI):**
210
+ ```yaml
211
+ - uses: snap-diff/snap_diff-capybara/.github/actions/setup-ruby-and-dependencies@master
212
+ - run: bundle exec rake test
213
+ - uses: snap-diff/snap_diff-capybara/.github/actions/upload-screenshots@master
214
+ if: failure()
215
+ with:
216
+ name: screenshots
217
+ pr-comment: 'true'
218
+ ```
219
+
220
+ ### Migration steps
221
+
222
+ 1. **Remove BackstopJS configuration** (`backstop.json`, npm dependencies)
223
+ 2. **Convert scenarios to Capybara tests** — each scenario becomes a `screenshot` call
224
+ 3. **Map threshold and delay settings** to `tolerance` and `stability_time_limit`
225
+ 4. **Run tests** to generate baselines
226
+ 5. **Commit baselines** (`git add doc/screenshots/`)
227
+ 6. **Update CI** to use the GitHub Actions setup
228
+
229
+ ---
230
+
231
+ ## General Migration Checklist
232
+
233
+ - [ ] Remove old gem/npm dependencies
234
+ - [ ] Add `capybara-screenshot-diff` to Gemfile
235
+ - [ ] Require the appropriate adapter (`minitest`, `rspec`, or `cucumber`)
236
+ - [ ] Replace screenshot calls with `screenshot` / `match_screenshot`
237
+ - [ ] Configure `window_size` for consistent viewport dimensions
238
+ - [ ] Set `tolerance` or `perceptual_threshold` if your previous tool had a mismatch threshold
239
+ - [ ] Add `.gitignore` patterns for diff artifacts
240
+ - [ ] Run tests to generate baseline screenshots
241
+ - [ ] Commit baselines to git
242
+ - [ ] Set up CI with artifact upload
243
+ - [ ] Optional: add HTML reporter and PR commenting
244
+
245
+ ## Common Gotchas
246
+
247
+ ### "My baselines are on Percy/Chromatic servers"
248
+
249
+ You'll need to take fresh screenshots. Either:
250
+ - Visit each page and capture manually
251
+ - Run tests with `RECORD_SCREENSHOTS=1` to generate all baselines at once
252
+
253
+ ### "I had hundreds of BackstopJS scenarios"
254
+
255
+ Start small. Migrate one test file at a time. The `screenshot_group` feature helps organize related screenshots:
256
+ ```ruby
257
+ screenshot_group 'checkout'
258
+ screenshot 'step1'
259
+ screenshot 'step2'
260
+ # Produces: doc/screenshots/checkout/00_step1.png, 01_step2.png
261
+ ```
262
+
263
+ ### "My tests are slow now"
264
+
265
+ Use the VIPS driver for ~50ms comparisons per image:
266
+ ```ruby
267
+ gem 'ruby-vips'
268
+ Capybara::Screenshot::Diff.driver = :vips
269
+ ```
270
+
271
+ ### "The diffs look different from what I'm used to"
272
+
273
+ Each tool uses different comparison algorithms:
274
+ - **Percy:** Proprietary pixel-level comparison with AI smoothing
275
+ - **Chromatic:** Visual catch algorithm (structure-aware)
276
+ - **BackstopJS:** Resemble.js pixel comparison
277
+ - **capybara-screenshot-diff:** Raw pixel difference with configurable tolerance
278
+
279
+ Start with default settings, then adjust `tolerance` or `perceptual_threshold` based on your needs.
280
+
281
+ ## Need Help?
282
+
283
+ - [Architecture Overview](docs/architecture.md) — understanding how comparisons work
284
+ - [Configuration Reference](docs/configuration.md) — all available options
285
+ - [CI Integration](docs/ci-integration.md) — setting up in CI
286
+ - [GitHub Issues](https://github.com/snap-diff/snap_diff-capybara/issues) — ask questions
@@ -0,0 +1,204 @@
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
+ **Note:** When a screenshot differs, diff artifacts (`.diff.png`, `.heatmap.diff.png`, etc.) are generated alongside the baseline. Add `*.diff.png`, `*.base.png`, `*.diff.webp`, `*.base.webp`, and `snap_diff_report.html` to your `.gitignore`. Clean up artifacts with `rake snap_diff:clean`.
37
+
38
+ ## Screenshot groups
39
+
40
+ Commonly it is useful to group screenshots around a feature, and record them as
41
+ a sequence. To do this, add a `screenshot_group` call to the start of your
42
+ test.
43
+
44
+ ```ruby
45
+ test 'my useful feature' do
46
+ screenshot_group 'useful_feature'
47
+ visit '/'
48
+ screenshot 'welcome_index'
49
+ click_button 'Useful feature'
50
+ screenshot 'feature_index'
51
+ click_button 'Perform action'
52
+ screenshot 'action_performed'
53
+ end
54
+ ```
55
+
56
+ This will produce a sequence of images like this
57
+
58
+ ```
59
+ doc
60
+ screenshots
61
+ useful_feature
62
+ 00_welcome_index
63
+ 01_feature_index
64
+ 02_action_performed
65
+ ```
66
+
67
+ **Note:** `screenshot_group` sets the group name for organizing screenshots. It does not delete existing files.
68
+
69
+
70
+ ## Screenshot sections
71
+
72
+ You can introduce another level above the screenshot group called a
73
+ `screenshot_section`. The section name is inserted just before the group name
74
+ in the save path. If called in the setup of the test, all screenshots in
75
+ that test will get the same prefix:
76
+
77
+ ```ruby
78
+ setup do
79
+ screenshot_section 'my_feature'
80
+ end
81
+
82
+ test 'my subfeature' do
83
+ screenshot_group 'subfeature'
84
+ visit '/feature'
85
+ click_button 'Interesting button'
86
+ screenshot 'subfeature_index'
87
+ click_button 'Perform action'
88
+ screenshot 'action_performed'
89
+ end
90
+ ```
91
+
92
+ This will produce a sequence of images like this
93
+
94
+ ```
95
+ doc
96
+ screenshots
97
+ my_feature
98
+ subfeature
99
+ 00_subfeature_index
100
+ 01_action_performed
101
+ ```
102
+
103
+
104
+ ## Setting `screenshot_section` and/or `screenshot_group` for all tests
105
+
106
+ Setting the `screenshot_section` and/or `screenshot_group` for all tests can be
107
+ done in the super class setup:
108
+
109
+ ```ruby
110
+ class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
111
+ setup do
112
+ screenshot_section class_name.underscore.sub(/(_feature|_system)?_test$/, '')
113
+ screenshot_group name[5..-1]
114
+ end
115
+ end
116
+ ```
117
+
118
+ `screenshot_section` and/or `screenshot_group` can still be overridden in each
119
+ test.
120
+
121
+
122
+ ## Capturing one area instead of the whole page
123
+
124
+ 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`
125
+
126
+ ```ruby
127
+ test 'the cool' do
128
+ visit '/feature'
129
+ screenshot 'cool_element', crop: '#my_element'
130
+ end
131
+ ```
132
+
133
+ **Note:** When using a retina device screenshots dimensions might be off. If
134
+ you are using (headless) chrome you can prevent this by setting the
135
+ `force-device-scale-factor` argument to `1`.
136
+
137
+ For Rails system specs using selenium you can do so for example by using the
138
+ following snippet:
139
+
140
+ ```ruby
141
+ driven_by :selenium, using: :chrome_headless do |options|
142
+ options.args << '--force-device-scale-factor=1'
143
+ end
144
+ ```
145
+
146
+ ## Multiple Capybara drivers
147
+
148
+ Often it is useful to test your app using different browsers. To avoid the
149
+ screenshots for different Capybara drivers to overwrite each other, set
150
+
151
+ ```ruby
152
+ Capybara::Screenshot.add_driver_path = true
153
+ ```
154
+
155
+ The example above will then save your screenshots like this
156
+ (for poltergeist and selenium):
157
+
158
+ ```
159
+ doc
160
+ screenshots
161
+ poltergeist
162
+ useful_feature
163
+ 00_welcome_index
164
+ 01_feature_index
165
+ 02_action_performed
166
+ selenium
167
+ useful_feature
168
+ 00_welcome_index
169
+ 01_feature_index
170
+ 02_action_performed
171
+ ```
172
+
173
+ ## Multiple OSs
174
+
175
+ If you run your tests on multiple operating systems, you will most likely find
176
+ the screen shots differ. To avoid the screenshots for different OSs to
177
+ overwrite each other, set
178
+
179
+ ```ruby
180
+ Capybara::Screenshot.add_os_path = true
181
+ ```
182
+
183
+ The example above will then save your screenshots like this
184
+ (for Linux and Windows):
185
+
186
+ ```
187
+ doc
188
+ screenshots
189
+ linux
190
+ useful_feature
191
+ 00_welcome_index
192
+ 01_feature_index
193
+ 02_action_performed
194
+ windows
195
+ useful_feature
196
+ 00_welcome_index
197
+ 01_feature_index
198
+ 02_action_performed
199
+ ```
200
+
201
+ If you combine this config with the `add_driver_path` config, the driver will be
202
+ put in front of the OS name.
203
+
204
+ [← 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,112 @@
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`
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.
data/gems.rb ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ source "https://rubygems.org"
4
+
5
+ # Specify your gem's dependencies in capybara-screenshot-diff.gemspec
6
+ gemspec path: __dir__
7
+
8
+ gem "rake"
9
+
10
+ # Image processing libraries
11
+ gem "chunky_png", ">= 1.3", require: false
12
+ gem "oily_png", platform: :ruby, git: "https://github.com/wvanbergen/oily_png", ref: "44042006e79efd42ce4b52c1d78a4c70f0b4b1b2"
13
+ gem "ruby-vips", require: false
14
+
15
+ group :test do
16
+ gem "capybara", ">= 3.26"
17
+ gem "mutex_m" # Needed for RubyMine debugging. Try removing it.
18
+ gem "minitest", "< 6", require: false
19
+ gem "minitest-mock", require: false
20
+ gem "minitest-stub-const", require: false
21
+ gem "simplecov", require: false
22
+ gem "rspec", require: false
23
+ end
24
+
25
+ # Capybara Server
26
+ gem "puma", require: false
27
+ gem "rackup", require: false
28
+
29
+ # Capybara Drivers
30
+ gem "cuprite", require: false
31
+ gem "selenium-webdriver", ">= 4.11", require: false
32
+
33
+ # Test Frameworks
34
+ # gem "cucumber", require: false
35
+ # gem "cucumber-rails", require: false
36
+
37
+ group :tools do
38
+ gem "standard", require: false
39
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
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"
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
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"