snap_diff-capybara 2.0.0.alpha1 → 2.0.0.beta2
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 +79 -0
- data/docs/UPGRADING.md +243 -1
- data/docs/architecture.md +91 -53
- data/docs/ci-integration.md +13 -3
- data/docs/configuration.md +31 -2
- data/docs/drivers.md +6 -0
- data/docs/framework-setup.md +21 -3
- data/docs/reporters.md +20 -5
- data/docs/snapdiff.md +326 -0
- data/docs/thread_safety.md +4 -3
- data/lib/capybara/screenshot/diff/config_legacy.rb +26 -72
- data/lib/capybara/screenshot/diff/image_compare.rb +5 -2
- data/lib/capybara/screenshot/diff/region.rb +3 -105
- data/lib/capybara/screenshot/diff/reporters/default.rb +4 -106
- data/lib/capybara_screenshot_diff/screenshot_assertion.rb +12 -24
- data/lib/capybara_screenshot_diff.rb +10 -4
- data/lib/snap_diff/area_calculator.rb +1 -3
- data/lib/snap_diff/browser_helpers.rb +1 -3
- data/lib/snap_diff/capture/viewport.rb +6 -7
- data/lib/snap_diff/comparison.rb +15 -28
- data/lib/snap_diff/config.rb +151 -29
- data/lib/snap_diff/deprecation.rb +26 -8
- data/lib/snap_diff/drivers.rb +20 -0
- data/lib/snap_diff/dsl.rb +12 -12
- data/lib/snap_diff/errors.rb +19 -0
- data/lib/snap_diff/integrations/cucumber.rb +5 -4
- data/lib/snap_diff/integrations/minitest.rb +11 -12
- data/lib/snap_diff/integrations/rspec.rb +7 -6
- data/lib/snap_diff/legacy_shims.rb +18 -0
- data/lib/snap_diff/region.rb +117 -0
- data/lib/snap_diff/reporters/default.rb +107 -0
- data/lib/snap_diff/reporters/html.rb +7 -9
- data/lib/snap_diff/reporting.rb +13 -4
- data/lib/snap_diff/screenshot_assertion.rb +37 -4
- data/lib/snap_diff/screenshot_matcher.rb +4 -4
- data/lib/snap_diff/screenshoter.rb +1 -1
- data/lib/snap_diff/snap_manager.rb +1 -2
- data/lib/snap_diff/stable_screenshoter.rb +2 -2
- data/lib/snap_diff/utils.rb +5 -3
- data/lib/snap_diff/version.rb +1 -1
- data/lib/snap_diff.rb +44 -15
- metadata +5 -1
data/docs/ci-integration.md
CHANGED
|
@@ -4,10 +4,19 @@
|
|
|
4
4
|
|
|
5
5
|
```ruby
|
|
6
6
|
# test/test_helper.rb
|
|
7
|
-
require '
|
|
7
|
+
require 'snap_diff/static'
|
|
8
|
+
|
|
9
|
+
SnapDiff.serve("_site") # or "public", "build", "dist"
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
<details>
|
|
13
|
+
<summary>Legacy names (still supported)</summary>
|
|
8
14
|
|
|
9
|
-
|
|
15
|
+
```ruby
|
|
16
|
+
require 'capybara_screenshot_diff/static'
|
|
17
|
+
CapybaraScreenshotDiff.serve("_site")
|
|
10
18
|
```
|
|
19
|
+
</details>
|
|
11
20
|
|
|
12
21
|
This sets up Capybara to serve static files and configures screenshot paths automatically.
|
|
13
22
|
|
|
@@ -24,7 +33,8 @@ Only commit the baseline screenshots (e.g., `homepage.png`). The `.base.png`, `.
|
|
|
24
33
|
Add to your test helper:
|
|
25
34
|
|
|
26
35
|
```ruby
|
|
27
|
-
require '
|
|
36
|
+
require 'snap_diff/reporters/html' # canonical
|
|
37
|
+
# require 'capybara_screenshot_diff/reporters/html' # legacy, same thing
|
|
28
38
|
```
|
|
29
39
|
|
|
30
40
|
### 2. Reusable composite action (recommended)
|
data/docs/configuration.md
CHANGED
|
@@ -2,10 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
## Quick Setup
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Canonical (v2):** every setting lives on one flat object, `SnapDiff.config`.
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
8
|
# In test_helper.rb or rails_helper.rb
|
|
9
|
+
SnapDiff.configure do |config|
|
|
10
|
+
config.window_size = [1280, 1024]
|
|
11
|
+
config.stability_time_limit = 1
|
|
12
|
+
config.blur_active_element = true
|
|
13
|
+
config.hide_caret = true
|
|
14
|
+
config.driver = :vips
|
|
15
|
+
config.tolerance = 0.0005
|
|
16
|
+
config.color_distance_limit = 15
|
|
17
|
+
end
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Legacy (still supported):** the two-holder block, split across `Capybara::Screenshot` and
|
|
21
|
+
`Capybara::Screenshot::Diff`.
|
|
22
|
+
|
|
23
|
+
```ruby
|
|
9
24
|
Capybara::Screenshot::Diff.configure do |screenshot, diff|
|
|
10
25
|
screenshot.window_size = [1280, 1024]
|
|
11
26
|
screenshot.stability_time_limit = 1
|
|
@@ -17,6 +32,20 @@ Capybara::Screenshot::Diff.configure do |screenshot, diff|
|
|
|
17
32
|
end
|
|
18
33
|
```
|
|
19
34
|
|
|
35
|
+
`SnapDiff::Config` **is** the storage; the legacy accessors are thin delegators onto it. There is
|
|
36
|
+
one source of truth, so a write through either surface is visible through the other — mixing them
|
|
37
|
+
is safe, and you can migrate a suite one line at a time:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
SnapDiff.config.window_size = [1280, 1024]
|
|
41
|
+
Capybara::Screenshot.window_size # => [1280, 1024]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Every option name below is identical on both surfaces — only the receiver changes. The one
|
|
45
|
+
exception: `Capybara::Screenshot.enabled` is `SnapDiff.config.screenshot_enabled`, because
|
|
46
|
+
`SnapDiff.config.enabled` is taken by `Capybara::Screenshot::Diff.enabled`. See
|
|
47
|
+
[SnapDiff — the canonical API](snapdiff.md) for the full SnapDiff-native surface.
|
|
48
|
+
|
|
20
49
|
**Note:** `fail_if_new` defaults to `true` in CI environments (when `ENV['CI']` is set). New screenshots are allowed locally but rejected in CI — no configuration needed.
|
|
21
50
|
|
|
22
51
|
**Note:** Setting `Capybara::Screenshot.enabled = false` is sufficient to disable all screenshots. There is no need to define no-op modules or monkey-patch the gem.
|
|
@@ -77,7 +106,7 @@ screenshot 'dashboard', color_distance_limit: 15
|
|
|
77
106
|
|
|
78
107
|
**Tier 1 — Zero config (works immediately):**
|
|
79
108
|
`blur_active_element`, `hide_caret`, and `fail_if_new` (in CI) are enabled by default.
|
|
80
|
-
Just `require '
|
|
109
|
+
Just `require 'snap_diff/integrations/minitest'` (legacy: `capybara_screenshot_diff/minitest`) and call `screenshot`.
|
|
81
110
|
|
|
82
111
|
**Tier 2 — Set when tests are flaky:**
|
|
83
112
|
|
data/docs/drivers.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Image Processing Drivers
|
|
2
2
|
|
|
3
|
+
> **Canonical equivalents.** Global settings shown here as
|
|
4
|
+
> `Capybara::Screenshot::Diff.<option> = …` are also `SnapDiff.config.<option> = …` — same
|
|
5
|
+
> option names, same storage, either surface works. Writing your own driver? See
|
|
6
|
+
> [Custom drivers](snapdiff.md#custom-drivers) for the `SnapDiff::Driver` mixin and how
|
|
7
|
+
> registration in `SnapDiff::Drivers.loaded` works.
|
|
8
|
+
|
|
3
9
|
## Perceptual color comparison (VIPS only)
|
|
4
10
|
|
|
5
11
|
By default, color differences are measured using raw RGB channel distance. This can produce
|
data/docs/framework-setup.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Framework Setup
|
|
2
2
|
|
|
3
|
+
> **Canonical equivalents.** This page uses the legacy `CapybaraScreenshotDiff` names, which keep
|
|
4
|
+
> working. Each has a `SnapDiff` home:
|
|
5
|
+
>
|
|
6
|
+
> | This page | Canonical |
|
|
7
|
+
> |-----------|-----------|
|
|
8
|
+
> | `require "capybara_screenshot_diff/minitest"` | `require "snap_diff/integrations/minitest"` |
|
|
9
|
+
> | `require "capybara_screenshot_diff/rspec"` | `require "snap_diff/integrations/rspec"` |
|
|
10
|
+
> | `require "capybara_screenshot_diff/cucumber"` | `require "snap_diff/integrations/cucumber"` |
|
|
11
|
+
> | `CapybaraScreenshotDiff::DSL` | `SnapDiff::DSL` |
|
|
12
|
+
> | `CapybaraScreenshotDiff::Minitest::Assertions` | `SnapDiff::Minitest::Assertions` |
|
|
13
|
+
> | `CapybaraScreenshotDiff.finalize_reporters!` | `SnapDiff::Reporting.finalize!` |
|
|
14
|
+
>
|
|
15
|
+
> The canonical setup, written out in full, is in [SnapDiff — the canonical API](snapdiff.md).
|
|
16
|
+
|
|
3
17
|
## Including DSL
|
|
4
18
|
|
|
5
19
|
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.
|
|
@@ -76,12 +90,16 @@ end
|
|
|
76
90
|
|
|
77
91
|
## Custom Test Frameworks
|
|
78
92
|
|
|
79
|
-
Minitest, RSpec, and Cucumber are supported out of the box. For other frameworks, call
|
|
93
|
+
Minitest, RSpec, and Cucumber are supported out of the box. For other frameworks, call the
|
|
94
|
+
end-of-suite hook yourself:
|
|
80
95
|
|
|
81
96
|
```ruby
|
|
82
|
-
|
|
97
|
+
SnapDiff::Reporting.finalize! # canonical
|
|
98
|
+
CapybaraScreenshotDiff.finalize_reporters! # legacy, same thing
|
|
83
99
|
```
|
|
84
100
|
|
|
85
|
-
This generates the HTML report and prints the summary.
|
|
101
|
+
This generates the HTML report and prints the summary. A framework also needs the per-test
|
|
102
|
+
lifecycle wired up — see
|
|
103
|
+
[Frameworks other than Minitest/RSpec/Cucumber](snapdiff.md#frameworks-other-than-minitestrspeccucumber).
|
|
86
104
|
|
|
87
105
|
[← Back to README](../README.md)
|
data/docs/reporters.md
CHANGED
|
@@ -6,7 +6,8 @@ Generate an interactive Web UI report of screenshot differences:
|
|
|
6
6
|
|
|
7
7
|
```ruby
|
|
8
8
|
# Add to test_helper.rb — one line, that's it
|
|
9
|
-
require '
|
|
9
|
+
require 'snap_diff/reporters/html' # canonical
|
|
10
|
+
# require 'capybara_screenshot_diff/reporters/html' # legacy, same thing
|
|
10
11
|
```
|
|
11
12
|
|
|
12
13
|
After running tests, open the report (generated only when there are failures):
|
|
@@ -21,7 +22,7 @@ The report includes a sidebar with thumbnails, side-by-side comparison with diff
|
|
|
21
22
|
|
|
22
23
|
## Custom Reporters
|
|
23
24
|
|
|
24
|
-
Build your own reporter by implementing `record` and `
|
|
25
|
+
Build your own reporter by implementing `record`, `finalize` and `summary`:
|
|
25
26
|
|
|
26
27
|
```ruby
|
|
27
28
|
class MyReporter
|
|
@@ -33,14 +34,28 @@ class MyReporter
|
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def finalize
|
|
36
|
-
# called once at
|
|
37
|
+
# called once at end of suite — write summary, upload report, etc.
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def summary
|
|
41
|
+
# printed to stdout after finalize; return nil to print nothing
|
|
42
|
+
nil
|
|
37
43
|
end
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
# Register in test_helper.rb
|
|
41
|
-
|
|
47
|
+
SnapDiff::Reporting.register(MyReporter.new) # canonical — appends under the mutex
|
|
48
|
+
# CapybaraScreenshotDiff.reporters << MyReporter.new # legacy, same list, skips the lock
|
|
42
49
|
```
|
|
43
50
|
|
|
44
|
-
Reporters are notified before assertions are cleared on each test teardown. `finalize`
|
|
51
|
+
Reporters are notified before assertions are cleared on each test teardown. `finalize` runs from
|
|
52
|
+
the framework's end-of-suite hook (`Minitest.after_run`, RSpec `after(:suite)`, Cucumber
|
|
53
|
+
`AfterAll`), which calls `SnapDiff::Reporting.finalize!`.
|
|
54
|
+
|
|
55
|
+
**Do implement `summary`.** `finalize!` calls it unconditionally, so a reporter without it is
|
|
56
|
+
finalized and then warned about. A reporter that raises is warned about and skipped — the others
|
|
57
|
+
still run.
|
|
58
|
+
|
|
59
|
+
Full details in [Custom reporters](snapdiff.md#custom-reporters).
|
|
45
60
|
|
|
46
61
|
[← Back to README](../README.md)
|
data/docs/snapdiff.md
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
# SnapDiff — the canonical API
|
|
2
|
+
|
|
3
|
+
Everything in this gem lives under `SnapDiff` since v2. This page is the SnapDiff-native
|
|
4
|
+
reference: setup, configuration, the object map, and the extension points — all using canonical
|
|
5
|
+
names only.
|
|
6
|
+
|
|
7
|
+
The legacy `Capybara::Screenshot::Diff` / `CapybaraScreenshotDiff` names still work (they resolve
|
|
8
|
+
to the same objects, with a one-time deprecation warning per constant), and the rest of the docs
|
|
9
|
+
still teach them. Nothing here replaces a working setup — it is what you write for **new** code.
|
|
10
|
+
For migrating an existing suite, see [UPGRADING.md](UPGRADING.md).
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
### Minitest
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
# test/test_helper.rb
|
|
18
|
+
require "snap_diff/integrations/minitest"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
```ruby
|
|
22
|
+
# test/application_system_test_case.rb
|
|
23
|
+
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
|
24
|
+
include SnapDiff::Minitest::Assertions # brings in SnapDiff::DSL too
|
|
25
|
+
end
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
```ruby
|
|
29
|
+
class HomepageTest < ApplicationSystemTestCase
|
|
30
|
+
test "homepage" do
|
|
31
|
+
visit "/"
|
|
32
|
+
assert_matches_screenshot "homepage"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
`SnapDiff::Minitest::Assertions` already includes `SnapDiff::DSL`, so a separate
|
|
38
|
+
`include SnapDiff::DSL` is not needed (it is harmless if you have it).
|
|
39
|
+
|
|
40
|
+
> **The require path is `snap_diff/integrations/…`, not `snap_diff/…`.**
|
|
41
|
+
> `require "snap_diff/minitest"` raises `LoadError` — there is no such file. The integrations
|
|
42
|
+
> live one level down, mirroring `lib/snap_diff/integrations/`.
|
|
43
|
+
|
|
44
|
+
### RSpec
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
# spec/rails_helper.rb
|
|
48
|
+
require "snap_diff/integrations/rspec"
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
This registers the `match_screenshot` matcher and includes `SnapDiff::DSL` into
|
|
52
|
+
`type: :feature` and `type: :system` examples automatically:
|
|
53
|
+
|
|
54
|
+
```ruby
|
|
55
|
+
RSpec.describe "Homepage", type: :system do
|
|
56
|
+
it "looks right" do
|
|
57
|
+
visit "/"
|
|
58
|
+
expect(page).to match_screenshot("homepage")
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
For other example types, include the DSL yourself:
|
|
64
|
+
|
|
65
|
+
```ruby
|
|
66
|
+
RSpec.describe "Admin", type: :request do
|
|
67
|
+
include SnapDiff::DSL
|
|
68
|
+
end
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Cucumber
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
# features/support/env.rb
|
|
75
|
+
require "snap_diff/integrations/cucumber"
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The DSL is added to the Cucumber `World`, so steps can call `screenshot` /
|
|
79
|
+
`assert_matches_screenshot` directly. This file must be loaded from inside a Cucumber run — it
|
|
80
|
+
calls `World`, `Before`, `After` and `AfterAll` at load time and raises `NoMethodError` if
|
|
81
|
+
required outside one.
|
|
82
|
+
|
|
83
|
+
### Static sites (Hugo, Jekyll, plain HTML)
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
require "snap_diff/static"
|
|
87
|
+
|
|
88
|
+
SnapDiff.serve("_site") # or "public", "build", "dist"
|
|
89
|
+
SnapDiff.serve("_site", root: Dir.pwd) # root defaults to Dir.pwd
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
`SnapDiff.serve` points Capybara at the built directory and sets the screenshot root. It also
|
|
93
|
+
loads the Minitest integration. See
|
|
94
|
+
[CI & Non-Rails Integration](ci-integration.md#non-rails-projects-hugo-jekyll-static-sites).
|
|
95
|
+
|
|
96
|
+
## Configuration
|
|
97
|
+
|
|
98
|
+
All 27 settings live on one flat object, `SnapDiff.config` (a `SnapDiff::Config`).
|
|
99
|
+
|
|
100
|
+
```ruby
|
|
101
|
+
# test_helper.rb / rails_helper.rb
|
|
102
|
+
SnapDiff.configure do |config|
|
|
103
|
+
config.window_size = [1280, 1024]
|
|
104
|
+
config.tolerance = 0.0005
|
|
105
|
+
config.driver = :vips
|
|
106
|
+
config.save_path = "doc/screenshots"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Or set them one at a time
|
|
110
|
+
SnapDiff.config.hide_caret = true
|
|
111
|
+
SnapDiff.config.tolerance # => 0.0005
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`SnapDiff::Config` **is** the storage. The legacy `Capybara::Screenshot.*` and
|
|
115
|
+
`Capybara::Screenshot::Diff.*` accessors are thin delegators onto it — one storage, two views —
|
|
116
|
+
so a write through either surface is immediately visible through the other:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
SnapDiff.config.window_size = [1280, 1024]
|
|
120
|
+
Capybara::Screenshot.window_size # => [1280, 1024]
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`SnapDiff.start` is the same call shape as the old `Capybara::Screenshot::Diff.configure`, if
|
|
124
|
+
you prefer the two-holder form:
|
|
125
|
+
|
|
126
|
+
```ruby
|
|
127
|
+
SnapDiff.start do |screenshot, diff|
|
|
128
|
+
screenshot.window_size = [1280, 1024]
|
|
129
|
+
diff.tolerance = 0.0005
|
|
130
|
+
end
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Three derived, read-only values are computed from the settings above:
|
|
134
|
+
|
|
135
|
+
| Method | What it returns |
|
|
136
|
+
|--------|-----------------|
|
|
137
|
+
| `SnapDiff.config.active?` | Whether screenshots are taken at all (ex `Capybara::Screenshot.active?`) |
|
|
138
|
+
| `SnapDiff.config.screenshot_area` | `save_path`, optionally segmented per OS and per Capybara driver |
|
|
139
|
+
| `SnapDiff.config.default_options` | The capture/compare defaults handed to `SnapDiff::Comparison` |
|
|
140
|
+
|
|
141
|
+
Every option's meaning is documented in the
|
|
142
|
+
[Configuration Reference](configuration.md) — the names are identical, only the receiver differs.
|
|
143
|
+
The one rename: `Capybara::Screenshot.enabled` is `SnapDiff.config.screenshot_enabled`, because
|
|
144
|
+
`SnapDiff.config.enabled` is taken by `Capybara::Screenshot::Diff.enabled`.
|
|
145
|
+
|
|
146
|
+
## Object map
|
|
147
|
+
|
|
148
|
+
`require "snap_diff"` gives you the compare/configure core. The test-suite pieces come with the
|
|
149
|
+
integration require; a few objects need their own require, noted below.
|
|
150
|
+
|
|
151
|
+
| Object | What it is for |
|
|
152
|
+
|--------|----------------|
|
|
153
|
+
| `SnapDiff.config`, `SnapDiff::Config` | Every setting, one flat object. The storage. |
|
|
154
|
+
| `SnapDiff.configure`, `SnapDiff.start` | Config block helpers (consolidated / v1 shape) |
|
|
155
|
+
| `SnapDiff.compare` | Compare two image files directly, no browser |
|
|
156
|
+
| `SnapDiff::Comparison` | The layered comparison engine (ex `ImageCompare`) |
|
|
157
|
+
| `SnapDiff::Comparison::Images` | Frozen bundle a comparison operates on: both images, their paths, the driver and the options |
|
|
158
|
+
| `SnapDiff::ComparisonResult` | Result value object: `different?`, region, metadata (ex `Difference`) |
|
|
159
|
+
| `SnapDiff::Region` | Bounding box value object — `from_edge_coordinates`, `to_edge_coordinates` |
|
|
160
|
+
| `SnapDiff::DSL` | `screenshot`, `assert_matches_screenshot`, `capture_screenshot`, groups/sections |
|
|
161
|
+
| `SnapDiff::Minitest::Assertions` | Minitest wiring (`snap_diff/integrations/minitest`) |
|
|
162
|
+
| `SnapDiff::Error` | Base class for every error this gem raises |
|
|
163
|
+
| `SnapDiff::ExpectationNotMet` | A screenshot did not match its baseline |
|
|
164
|
+
| `SnapDiff::UnstableImage` | No stable capture within `stability_time_limit` / `wait` |
|
|
165
|
+
| `SnapDiff::WindowSizeMismatchError` | Browser window is not the configured `window_size` |
|
|
166
|
+
| `SnapDiff::Driver` | Mixin with the shared driver defaults (`require "snap_diff/driver"`) |
|
|
167
|
+
| `SnapDiff::Drivers` | Driver factory and registry — `.for`, `.loaded`, `.available` |
|
|
168
|
+
| `SnapDiff::Reporting` | Process-global reporter lifecycle (`require "snap_diff/reporting"`) |
|
|
169
|
+
| `SnapDiff::Reporters::HTML` | The interactive HTML report (`require "snap_diff/reporters/html"`) |
|
|
170
|
+
| `SnapDiff::Reporters::Default` | Builds the annotated diff images and the failure message |
|
|
171
|
+
| `SnapDiff.session` | The per-test assertion registry (fiber-local) |
|
|
172
|
+
| `SnapDiff.reset` | Ends a test: notifies reporters, clears the session |
|
|
173
|
+
| `SnapDiff.pending_screenshots_message` | Skip message when a new screenshot has no baseline |
|
|
174
|
+
| `SnapDiff::Capture::Viewport` | Per-capture viewport check seam (`require "snap_diff/capture/viewport"`) |
|
|
175
|
+
| `SnapDiff.serve` | Point Capybara at a static site directory (`require "snap_diff/static"`) |
|
|
176
|
+
|
|
177
|
+
## Compare two images without a browser
|
|
178
|
+
|
|
179
|
+
Works on anything on disk — rendered PDFs, generated charts, CI artifacts:
|
|
180
|
+
|
|
181
|
+
```ruby
|
|
182
|
+
require "snap_diff"
|
|
183
|
+
|
|
184
|
+
result = SnapDiff.compare("baseline.png", "current.png")
|
|
185
|
+
result.quick_equal? # => true when byte-identical / pixel-identical
|
|
186
|
+
result.different? # => true when the difference exceeds the configured thresholds
|
|
187
|
+
result.difference # => SnapDiff::ComparisonResult with region and metadata
|
|
188
|
+
|
|
189
|
+
# Per-call option overrides, merged over SnapDiff.config.default_options
|
|
190
|
+
SnapDiff.compare("baseline.png", "current.png", tolerance: 0.5)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Note the argument order: baseline first, current second ("compare baseline against current").
|
|
194
|
+
|
|
195
|
+
## Custom reporters
|
|
196
|
+
|
|
197
|
+
A reporter is any object answering `record(assertions)`, `finalize`, and `summary`. Register it
|
|
198
|
+
once, for the rest of the process:
|
|
199
|
+
|
|
200
|
+
```ruby
|
|
201
|
+
require "snap_diff/reporting"
|
|
202
|
+
|
|
203
|
+
class SlackReporter
|
|
204
|
+
def initialize = @failures = []
|
|
205
|
+
|
|
206
|
+
# Called once per finished test, with that test's assertions.
|
|
207
|
+
def record(assertions)
|
|
208
|
+
assertions.each do |assertion|
|
|
209
|
+
next unless assertion.compare&.difference&.different?
|
|
210
|
+
@failures << assertion.name
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Called once at end of suite.
|
|
215
|
+
def finalize
|
|
216
|
+
post_to_slack(@failures) unless @failures.empty?
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
# Printed to stdout after finalize. Return nil to print nothing.
|
|
220
|
+
def summary
|
|
221
|
+
@failures.empty? ? nil : "#{@failures.size} screenshot(s) changed"
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
SnapDiff::Reporting.register(SlackReporter.new)
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
`register` appends under a mutex, so concurrent registrations cannot lose one — prefer it over
|
|
229
|
+
mutating `SnapDiff::Reporting.reporters` directly.
|
|
230
|
+
|
|
231
|
+
Do implement `summary`. `finalize!` calls it unconditionally; a reporter without it is finalized
|
|
232
|
+
and then warned about (`[snap_diff] Reporter … failed (NoMethodError: undefined method 'summary')`).
|
|
233
|
+
|
|
234
|
+
A reporter that raises is warned about and skipped — the other reporters still run.
|
|
235
|
+
|
|
236
|
+
The bundled HTML report is just a pre-registered reporter of this kind:
|
|
237
|
+
|
|
238
|
+
```ruby
|
|
239
|
+
require "snap_diff/reporters/html" # registers SnapDiff::Reporters::HTML itself
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Frameworks other than Minitest/RSpec/Cucumber
|
|
243
|
+
|
|
244
|
+
The three bundled integrations call the lifecycle for you. Wiring another framework means calling
|
|
245
|
+
three things:
|
|
246
|
+
|
|
247
|
+
```ruby
|
|
248
|
+
require "snap_diff/dsl" # SnapDiff::DSL, BrowserHelpers, the session accessors
|
|
249
|
+
require "snap_diff/reporting" # SnapDiff::Reporting
|
|
250
|
+
|
|
251
|
+
# before each test
|
|
252
|
+
SnapDiff::BrowserHelpers.resize_window_if_needed
|
|
253
|
+
|
|
254
|
+
# after each test
|
|
255
|
+
SnapDiff.session.verify # raises SnapDiff::ExpectationNotMet on a mismatch
|
|
256
|
+
msg = SnapDiff.pending_screenshots_message # non-nil => skip the test with this message
|
|
257
|
+
SnapDiff.reset # always: notifies reporters, clears the session
|
|
258
|
+
|
|
259
|
+
# after the suite
|
|
260
|
+
SnapDiff::Reporting.finalize!
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Custom drivers
|
|
264
|
+
|
|
265
|
+
A driver is a plain object that does the image work. Include `SnapDiff::Driver` for the shared
|
|
266
|
+
defaults, then implement the operations the comparison engine calls:
|
|
267
|
+
|
|
268
|
+
```ruby
|
|
269
|
+
require "snap_diff/driver"
|
|
270
|
+
|
|
271
|
+
class MyDriver
|
|
272
|
+
include SnapDiff::Driver
|
|
273
|
+
|
|
274
|
+
# Provided by the mixin, override only if your image objects differ:
|
|
275
|
+
# width_for(image), height_for(image), dimension(image),
|
|
276
|
+
# image_area_size(image), same_dimension?(comparison), supports?(feature)
|
|
277
|
+
|
|
278
|
+
# Implement (this is what both bundled drivers implement):
|
|
279
|
+
# from_file(path), load_images(base_path, new_path), save_image_to(image, path)
|
|
280
|
+
# same_pixels?(comparison), find_difference_region(comparison)
|
|
281
|
+
# crop(region, image), resize_image_to(image, w, h)
|
|
282
|
+
# add_black_box(image, region), draw_rectangles(images, region, ...)
|
|
283
|
+
end
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
`supports?(feature)` is just `respond_to?(feature)` — the engine uses it to skip optional
|
|
287
|
+
operations. The VIPS driver additionally implements `filter_image_with_median`, `merge`,
|
|
288
|
+
`highlight_mask` and `difference_level`; ChunkyPNG does not, and `supports?` is how that is
|
|
289
|
+
detected. Look at `lib/snap_diff/drivers/chunky_png_driver.rb` for the smaller of the two
|
|
290
|
+
reference implementations.
|
|
291
|
+
|
|
292
|
+
### Registration
|
|
293
|
+
|
|
294
|
+
`SnapDiff::Drivers.loaded` is the registry: a mutable `name => driver class` hash. Register by
|
|
295
|
+
writing into it, then select the driver by that name:
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
SnapDiff::Drivers.loaded[:my_driver] = MyDriver
|
|
299
|
+
|
|
300
|
+
SnapDiff.config.driver = :my_driver # globally
|
|
301
|
+
screenshot "index", driver: :my_driver # or per screenshot
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Resolution goes through `SnapDiff::Drivers.for`, which looks the symbol up in `loaded` and calls
|
|
305
|
+
`.new` on the class — **your driver class must be instantiable with no arguments**. A pre-built
|
|
306
|
+
instance skips the registry entirely:
|
|
307
|
+
|
|
308
|
+
```ruby
|
|
309
|
+
screenshot "index", driver: MyDriver.new # any non-Symbol is used as-is
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
`SnapDiff::Drivers.available` is the *detected* list (`[:vips, :chunky_png]`, filled at load time
|
|
313
|
+
by probing for the `ruby-vips` and `chunky_png` gems). It is a read-only view of detection, not
|
|
314
|
+
the registry — registering a custom driver does not add it there, and `driver: :auto` picks
|
|
315
|
+
`available.first`. Custom drivers must always be named explicitly.
|
|
316
|
+
|
|
317
|
+
## Related
|
|
318
|
+
|
|
319
|
+
- [Framework Setup](framework-setup.md) — the same three integrations under their legacy names
|
|
320
|
+
- [Configuration Reference](configuration.md) — what every option does
|
|
321
|
+
- [Image Processing Drivers](drivers.md) — VIPS vs ChunkyPNG, perceptual threshold
|
|
322
|
+
- [Web UI & Custom Reporters](reporters.md) — the HTML report in detail
|
|
323
|
+
- [Architecture](architecture.md) — how the pieces fit together internally
|
|
324
|
+
- [UPGRADING.md](UPGRADING.md) — migrating an existing suite off the legacy names
|
|
325
|
+
|
|
326
|
+
[← Back to README](../README.md)
|
data/docs/thread_safety.md
CHANGED
|
@@ -100,9 +100,10 @@ Do not:
|
|
|
100
100
|
|
|
101
101
|
Runtime state is thread-local (above), but *loading* the gem is a separate
|
|
102
102
|
concern. The require graph is deliberately acyclic: `lib/snap_diff/*` units
|
|
103
|
-
depend only on the
|
|
104
|
-
|
|
105
|
-
umbrella files depend on the units, and nothing requires back up
|
|
103
|
+
depend only on the config-storage leaf (`snap_diff/config`, which the legacy
|
|
104
|
+
view `capybara/screenshot/diff/config_legacy` requires) and specific sibling
|
|
105
|
+
units, the umbrella files depend on the units, and nothing requires back up
|
|
106
|
+
the chain.
|
|
106
107
|
|
|
107
108
|
Eager mutual requires between entry points are forbidden, even guarded ones:
|
|
108
109
|
per-thread "loading" flags cannot serialize Ruby's process-global per-file
|
|
@@ -1,80 +1,44 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Legacy Capybara::Screenshot / Capybara::Screenshot::Diff
|
|
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.
|
|
3
|
+
# Legacy Capybara::Screenshot / Capybara::Screenshot::Diff config surface.
|
|
8
4
|
#
|
|
9
|
-
#
|
|
10
|
-
# (
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
require "
|
|
20
|
-
|
|
5
|
+
# Since ADR-008 step 1 the storage lives in SnapDiff::Config -- the require
|
|
6
|
+
# leaf of the config graph (see its own header) -- and since step 7b the
|
|
7
|
+
# DERIVED values (active?, screenshot_area, default_options) live there
|
|
8
|
+
# too. snap_diff/config.rb also generates the old accessor names as thin
|
|
9
|
+
# delegators from SnapDiff::Config::MAPPING, so nothing but forwarders is
|
|
10
|
+
# left here. The v1 surface (Capybara::Screenshot.window_size = ...,
|
|
11
|
+
# Diff.configure { ... }, Diff.compare) keeps working unchanged: one
|
|
12
|
+
# storage, two views.
|
|
13
|
+
#
|
|
14
|
+
# Load order: requiring snap_diff/config first also eagerly evaluates the
|
|
15
|
+
# require-time defaults (ENV["CI"] for fail_if_new, Rails.root/pwd for
|
|
16
|
+
# root) at this same load moment, exactly when the old mattr_accessor
|
|
17
|
+
# default blocks used to run. snap_diff/config never requires back here,
|
|
18
|
+
# so the graph stays acyclic.
|
|
19
|
+
require "snap_diff/config"
|
|
20
|
+
# AVAILABLE_DRIVERS below is evaluated at class-body eval time, so Utils
|
|
21
|
+
# must be a real, already-loaded module before this module body runs.
|
|
21
22
|
require "snap_diff/utils"
|
|
22
23
|
|
|
23
24
|
module Capybara
|
|
24
25
|
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
26
|
class << self
|
|
40
|
-
def root=(path)
|
|
41
|
-
@@root = Pathname(path).expand_path
|
|
42
|
-
end
|
|
43
|
-
|
|
44
27
|
def active?
|
|
45
|
-
|
|
28
|
+
SnapDiff.config.active?
|
|
46
29
|
end
|
|
47
30
|
|
|
48
31
|
def screenshot_area
|
|
49
|
-
|
|
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)
|
|
32
|
+
SnapDiff.config.screenshot_area
|
|
53
33
|
end
|
|
54
34
|
|
|
55
35
|
def screenshot_area_abs
|
|
56
|
-
|
|
36
|
+
SnapDiff.config.screenshot_area_abs
|
|
57
37
|
end
|
|
58
38
|
end
|
|
59
39
|
|
|
60
40
|
# Module to track screenshot changes
|
|
61
41
|
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
42
|
AVAILABLE_DRIVERS = SnapDiff::Utils.detect_available_drivers.freeze
|
|
79
43
|
|
|
80
44
|
# Configure screenshot and diff settings in one block.
|
|
@@ -85,28 +49,18 @@ module Capybara
|
|
|
85
49
|
# diff.driver = :vips
|
|
86
50
|
# diff.tolerance = 0.0005
|
|
87
51
|
# end
|
|
52
|
+
# The bare `yield` (rather than an explicit &block) keeps this
|
|
53
|
+
# method's published arity byte-identical to what it always had.
|
|
88
54
|
def self.configure
|
|
89
|
-
yield
|
|
55
|
+
SnapDiff.start { |screenshot, diff| yield screenshot, diff }
|
|
90
56
|
end
|
|
91
57
|
|
|
92
58
|
def self.compare(baseline_path, current_path, **options)
|
|
93
|
-
SnapDiff
|
|
59
|
+
SnapDiff.compare(baseline_path, current_path, **options)
|
|
94
60
|
end
|
|
95
61
|
|
|
96
62
|
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
|
-
}
|
|
63
|
+
SnapDiff.config.default_options
|
|
110
64
|
end
|
|
111
65
|
end
|
|
112
66
|
end
|