capybara-screenshot-diff 1.15.0 → 2.0.0.alpha1
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 +59 -0
- data/docs/RELEASE_PREP.md +6 -6
- data/docs/thread_safety.md +15 -0
- data/lib/capybara/screenshot/diff/annotation_service.rb +5 -77
- data/lib/capybara/screenshot/diff/area_calculator.rb +5 -54
- data/lib/capybara/screenshot/diff/browser_helpers.rb +5 -119
- data/lib/capybara/screenshot/diff/config_legacy.rb +113 -0
- data/lib/capybara/screenshot/diff/cucumber.rb +1 -1
- data/lib/capybara/screenshot/diff/difference.rb +6 -102
- data/lib/capybara/screenshot/diff/drivers/base_driver.rb +7 -39
- data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +5 -299
- data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +5 -169
- data/lib/capybara/screenshot/diff/drivers.rb +7 -14
- data/lib/capybara/screenshot/diff/image_compare.rb +10 -228
- data/lib/capybara/screenshot/diff/image_preprocessor.rb +5 -70
- data/lib/capybara/screenshot/diff/os.rb +8 -13
- data/lib/capybara/screenshot/diff/reporters/default.rb +5 -3
- data/lib/capybara/screenshot/diff/screenshot_matcher.rb +5 -129
- data/lib/capybara/screenshot/diff/screenshoter.rb +5 -127
- data/lib/capybara/screenshot/diff/stable_screenshoter.rb +5 -104
- data/lib/capybara/screenshot/diff/utils.rb +5 -40
- data/lib/capybara/screenshot/diff/vcs.rb +5 -37
- data/lib/capybara/screenshot/diff/version.rb +7 -1
- data/lib/capybara_screenshot_diff/attempts_reporter.rb +5 -47
- data/lib/capybara_screenshot_diff/cucumber.rb +5 -17
- data/lib/capybara_screenshot_diff/dsl.rb +7 -129
- data/lib/capybara_screenshot_diff/error_with_filtered_backtrace.rb +6 -30
- data/lib/capybara_screenshot_diff/minitest.rb +6 -56
- data/lib/capybara_screenshot_diff/reporters/html.rb +5 -135
- data/lib/capybara_screenshot_diff/rspec.rb +5 -63
- data/lib/capybara_screenshot_diff/screenshot_assertion.rb +39 -157
- data/lib/capybara_screenshot_diff/screenshot_namer.rb +5 -79
- data/lib/capybara_screenshot_diff/snap.rb +5 -64
- data/lib/capybara_screenshot_diff/snap_manager.rb +5 -82
- data/lib/capybara_screenshot_diff/static.rb +3 -5
- data/lib/capybara_screenshot_diff.rb +29 -100
- data/lib/snap_diff/annotation_service.rb +84 -0
- data/lib/snap_diff/area_calculator.rb +56 -0
- data/lib/snap_diff/attempts_reporter.rb +51 -0
- data/lib/snap_diff/browser_helpers.rb +121 -0
- data/lib/snap_diff/capture/viewport.rb +40 -0
- data/lib/snap_diff/comparison.rb +238 -0
- data/lib/snap_diff/comparison_result.rb +104 -0
- data/lib/snap_diff/config.rb +6 -2
- data/lib/snap_diff/deprecation.rb +84 -0
- data/lib/snap_diff/driver.rb +37 -0
- data/lib/snap_diff/drivers/chunky_png_driver.rb +298 -0
- data/lib/snap_diff/drivers/vips_driver.rb +171 -0
- data/lib/snap_diff/drivers.rb +14 -0
- data/lib/snap_diff/dsl.rb +143 -0
- data/lib/snap_diff/error_with_filtered_backtrace.rb +32 -0
- data/lib/snap_diff/image_preprocessor.rb +68 -0
- data/lib/snap_diff/integrations/cucumber.rb +22 -0
- data/lib/snap_diff/integrations/minitest.rb +70 -0
- data/lib/snap_diff/integrations/rspec.rb +68 -0
- data/lib/snap_diff/legacy_shims.rb +99 -0
- data/lib/snap_diff/os.rb +17 -0
- data/lib/snap_diff/reporters/html.rb +143 -0
- data/lib/snap_diff/reporting.rb +53 -0
- data/lib/snap_diff/screenshot_assertion.rb +143 -0
- data/lib/snap_diff/screenshot_matcher.rb +113 -0
- data/lib/snap_diff/screenshot_namer.rb +81 -0
- data/lib/snap_diff/screenshoter.rb +129 -0
- data/lib/snap_diff/snap.rb +66 -0
- data/lib/snap_diff/snap_manager.rb +125 -0
- data/lib/snap_diff/stable_screenshoter.rb +103 -0
- data/lib/snap_diff/static.rb +11 -0
- data/lib/snap_diff/utils.rb +38 -0
- data/lib/snap_diff/vcs.rb +35 -0
- data/lib/snap_diff/version.rb +5 -0
- data/lib/snap_diff.rb +22 -6
- metadata +36 -2
- /data/lib/{capybara_screenshot_diff → snap_diff}/reporters/templates/report.html.erb +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 183664107a9aaffa1f5e89fb5c4ba3b74e798d856ed22d3256a3d3b03e528231
|
|
4
|
+
data.tar.gz: b2a226430eaeab196e297efc9c476eb4cf0d192334d1503ba29f330fb9bd81ed
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: fda1cec9f451076ee8260b3f9d3777b1c384e6ba5ca7119a08b265cb5fec23ffe6c7315e6f97feb3c2199ab9dcf99d827eda54e3cd0857c671e270016af61af3
|
|
7
|
+
data.tar.gz: 2651d17eaceca6a6b8f5c94136e8e221639ec5cf70d70d9178d9d30a630f5014e5eb240ace31233a45a456cf3918d0e70b324c998e2b4ab3531bac7d7d21ffe0
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,65 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [v2.0.0.alpha1] - 2026-08-22
|
|
9
|
+
|
|
10
|
+
**Opt-in experiment prerelease.** RubyGems never installs prereleases by default
|
|
11
|
+
resolution — to try it: `gem "capybara-screenshot-diff", "2.0.0.alpha1"`. The
|
|
12
|
+
final 2.0.0 ships only after adopter feedback on this train; please report
|
|
13
|
+
anything surprising on [#166](https://github.com/snap-diff/snap_diff-capybara/issues/166).
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **`SnapDiff` is the canonical namespace.** The implementation lives in
|
|
17
|
+
`lib/snap_diff/`; every legacy constant (`Capybara::Screenshot::Diff::*`,
|
|
18
|
+
`CapybaraScreenshotDiff::*`) still resolves to the *same object* and keeps
|
|
19
|
+
working ([#208](https://github.com/snap-diff/snap_diff-capybara/pull/208),
|
|
20
|
+
[#209](https://github.com/snap-diff/snap_diff-capybara/pull/209),
|
|
21
|
+
[#210](https://github.com/snap-diff/snap_diff-capybara/pull/210)):
|
|
22
|
+
`ImageCompare` → `SnapDiff::Comparison`, `Difference` →
|
|
23
|
+
`SnapDiff::ComparisonResult`, `Drivers::BaseDriver` → `SnapDiff::Driver` (mixin).
|
|
24
|
+
- **Legacy constants warn on first use** — once per constant per process, naming
|
|
25
|
+
the replacement ([#218](https://github.com/snap-diff/snap_diff-capybara/pull/218)).
|
|
26
|
+
Silence with `SnapDiff.silence_deprecations = true` or
|
|
27
|
+
`SNAP_DIFF_SILENCE_DEPRECATIONS=1`. A few constants stay silent by design
|
|
28
|
+
(`Os`, `DSL`, `VERSION`, driver leaf classes — see `lib/snap_diff/legacy_shims.rb`).
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- Annotation color constants resolve under the bare `require "snap_diff"` entry;
|
|
32
|
+
previously a differing comparison raised `NameError` there
|
|
33
|
+
([#219](https://github.com/snap-diff/snap_diff-capybara/pull/219))
|
|
34
|
+
|
|
35
|
+
### Known caveats (deliberate, alpha)
|
|
36
|
+
- `defined?(...)` / `const_defined?` on lazily-shimmed legacy names returns
|
|
37
|
+
false/nil — feature detection like `defined?(Capybara::Screenshot::Diff::Drivers::VipsDriver)`
|
|
38
|
+
must move to the `SnapDiff::` name. Rescuing legacy error classes is unaffected
|
|
39
|
+
(they remain eagerly defined).
|
|
40
|
+
- Reopening `module Capybara::Screenshot::Diff::Drivers` (historical custom-driver
|
|
41
|
+
monkey-patch pattern) defines a fresh module that shadows the shim; define custom
|
|
42
|
+
drivers under `SnapDiff::Drivers` instead.
|
|
43
|
+
|
|
44
|
+
### Internal
|
|
45
|
+
- Core simplification, behavior-preserving (the "5.5-lite" pass): pure
|
|
46
|
+
capture/comparison option partition ([#212](https://github.com/snap-diff/snap_diff-capybara/pull/212)),
|
|
47
|
+
stability failure as data ([#213](https://github.com/snap-diff/snap_diff-capybara/pull/213)),
|
|
48
|
+
explicit `ScreenshotAssertion#archive_baseline!` + `#inspect` on
|
|
49
|
+
assertion/result ([#214](https://github.com/snap-diff/snap_diff-capybara/pull/214)),
|
|
50
|
+
`SnapDiff::Capture::Viewport` seam with the future `anchor:` hook
|
|
51
|
+
([#215](https://github.com/snap-diff/snap_diff-capybara/pull/215)), reporter vs
|
|
52
|
+
session lifecycle separation ([#216](https://github.com/snap-diff/snap_diff-capybara/pull/216)),
|
|
53
|
+
all behind a guard-test net ([#211](https://github.com/snap-diff/snap_diff-capybara/pull/211))
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## [v1.15.1] - 2026-08-22
|
|
58
|
+
|
|
59
|
+
### Fixed
|
|
60
|
+
- **`VipsDriver#resize_image_to` resizes to the requested dimensions** — the vips driver passed a target aspect ratio where libvips expects a scale factor, so retina-halving on macOS/Selenium could enlarge screenshots (2560×1600 → 4096×2560) and save wrongly-sized baselines; now scales width and height independently via `resize(scale, vscale:)` ([#205](https://github.com/snap-diff/snap_diff-capybara/pull/205))
|
|
61
|
+
|
|
62
|
+
### Internal
|
|
63
|
+
- Driver contract tests pinning the shared ChunkyPNG/Vips driver seam (method surface, load/compare behavior, option handling) — the net that caught the resize bug ([#204](https://github.com/snap-diff/snap_diff-capybara/pull/204))
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
8
67
|
## [v1.15.0] - 2026-08-22
|
|
9
68
|
|
|
10
69
|
### Added
|
data/docs/RELEASE_PREP.md
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
# Release Preparation — v1.15.
|
|
1
|
+
# Release Preparation — v1.15.1
|
|
2
2
|
|
|
3
3
|
## Summary
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
HIGH-severity VipsDriver resize fix (retina halving could enlarge screenshots
|
|
6
|
+
and corrupt baselines) plus the driver contract tests that caught it.
|
|
7
7
|
|
|
8
8
|
## Release Checklist
|
|
9
9
|
|
|
10
10
|
### Pre-Release
|
|
11
11
|
|
|
12
|
-
- [x] Update version to `1.15.
|
|
13
|
-
- [x] Run tests: `bundle exec rake test:unit` (
|
|
12
|
+
- [x] Update version to `1.15.1`
|
|
13
|
+
- [x] Run tests: `bundle exec rake test:unit` (376 runs, 0 failures)
|
|
14
14
|
- [x] Update CHANGELOG.md
|
|
15
15
|
|
|
16
16
|
### Release (One Click)
|
|
17
17
|
|
|
18
18
|
1. Push to GitHub
|
|
19
19
|
2. Go to [Actions → Release](https://github.com/snap-diff/snap_diff-capybara/actions/workflows/release.yml)
|
|
20
|
-
3. Click **Run workflow**, enter `1.15.
|
|
20
|
+
3. Click **Run workflow**, enter `1.15.1`
|
|
21
21
|
4. Workflow will: test → tag → publish to RubyGems → create GitHub Release
|
|
22
22
|
|
|
23
23
|
### Post-Release
|
data/docs/thread_safety.md
CHANGED
|
@@ -95,3 +95,18 @@ Do not:
|
|
|
95
95
|
- Paths are unique per screenshot name and counter
|
|
96
96
|
- `FileUtils.mv` is atomic on most file systems
|
|
97
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.
|
|
@@ -1,79 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
# builds error messages; this class owns all image annotation work.
|
|
9
|
-
class AnnotationService
|
|
10
|
-
attr_reader :annotated_image_path, :annotated_base_image_path, :heatmap_diff_path
|
|
11
|
-
|
|
12
|
-
def initialize(difference)
|
|
13
|
-
@difference = difference
|
|
14
|
-
|
|
15
|
-
comparison = difference.comparison
|
|
16
|
-
ext = comparison.new_image_path.extname.delete_prefix(".")
|
|
17
|
-
screenshot_format = comparison.options[:screenshot_format] || (ext unless ext.empty?) || "png"
|
|
18
|
-
@annotated_image_path = comparison.new_image_path.sub_ext(".diff.#{screenshot_format}")
|
|
19
|
-
@annotated_base_image_path = comparison.base_image_path.sub_ext(".diff.#{screenshot_format}")
|
|
20
|
-
@heatmap_diff_path = comparison.new_image_path.sub_ext(".heatmap.diff.#{screenshot_format}")
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def clean_tmp_files
|
|
24
|
-
annotated_base_image_path.unlink if annotated_base_image_path.exist?
|
|
25
|
-
annotated_image_path.unlink if annotated_image_path.exist?
|
|
26
|
-
heatmap_diff_path.unlink if heatmap_diff_path.exist?
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
def annotate_and_save_images
|
|
30
|
-
save_annotation_for(new_image, annotated_image_path)
|
|
31
|
-
save_annotation_for(base_image, annotated_base_image_path)
|
|
32
|
-
save_heatmap_diff if difference.diff_mask
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def save_annotation_for(image, image_path)
|
|
36
|
-
image = annotate_difference(image, difference.region)
|
|
37
|
-
image = annotate_skip_areas(image, difference.comparison.skip_area) if difference.comparison.skip_area
|
|
38
|
-
|
|
39
|
-
save(image, image_path.to_path)
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def annotate_difference(image, region)
|
|
43
|
-
driver.draw_rectangles([image], region, CapybaraScreenshotDiff::RED_RGBA, offset: 1).first
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def annotate_skip_areas(image, skip_areas)
|
|
47
|
-
skip_areas.reduce(image) do |memo, region|
|
|
48
|
-
driver.draw_rectangles([memo], region, CapybaraScreenshotDiff::ORANGE_RGBA).first
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def save(image, image_path)
|
|
53
|
-
driver.save_image_to(image, image_path.to_s)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
attr_reader :difference
|
|
59
|
-
|
|
60
|
-
def save_heatmap_diff
|
|
61
|
-
merged_image = driver.merge(new_image, base_image)
|
|
62
|
-
highlighted_mask = driver.highlight_mask(difference.diff_mask, merged_image, color: CapybaraScreenshotDiff::RED_RGBA)
|
|
63
|
-
|
|
64
|
-
save(highlighted_mask, heatmap_diff_path.to_path)
|
|
65
|
-
end
|
|
66
|
-
|
|
67
|
-
def base_image
|
|
68
|
-
difference.comparison.base_image
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def new_image
|
|
72
|
-
difference.comparison.new_image
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def driver
|
|
76
|
-
@_driver ||= difference.comparison.driver
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
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"
|
|
@@ -1,56 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
@crop_coordinates = crop_coordinates
|
|
9
|
-
@skip_area = skip_area
|
|
10
|
-
end
|
|
11
|
-
|
|
12
|
-
def calculate_crop
|
|
13
|
-
return @_calculated_crop if defined?(@_calculated_crop)
|
|
14
|
-
return @_calculated_crop = nil unless @crop_coordinates
|
|
15
|
-
|
|
16
|
-
# TODO: Move out from this class, this should be done on before screenshot and should not depend on Browser
|
|
17
|
-
@crop_coordinates = BrowserHelpers.bounds_for_css(@crop_coordinates).first if @crop_coordinates.is_a?(String)
|
|
18
|
-
@_calculated_crop = Region.from_edge_coordinates(*@crop_coordinates)
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
# Cast skip areas params into Region
|
|
22
|
-
# and if there is crop then makes absolute coordinates to eb relative to crop top left corner
|
|
23
|
-
def calculate_skip_area
|
|
24
|
-
return nil unless @skip_area
|
|
25
|
-
|
|
26
|
-
crop_region = calculate_crop
|
|
27
|
-
skip_area = Array(@skip_area)
|
|
28
|
-
|
|
29
|
-
css_selectors, coords_list = skip_area.compact.partition { |region| region.is_a? String }
|
|
30
|
-
regions, coords_list = coords_list.partition { |region| region.is_a? Region }
|
|
31
|
-
|
|
32
|
-
regions.concat(build_regions_for(BrowserHelpers.bounds_for_css(*css_selectors))) unless css_selectors.empty?
|
|
33
|
-
regions.concat(build_regions_for(coords_list.flatten.each_slice(4))) unless coords_list.empty?
|
|
34
|
-
|
|
35
|
-
regions.compact!
|
|
36
|
-
|
|
37
|
-
if crop_region
|
|
38
|
-
regions
|
|
39
|
-
.map! { |region| crop_region.find_relative_intersect(region) }
|
|
40
|
-
.filter! { |region| region&.present? }
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
regions
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
private
|
|
47
|
-
|
|
48
|
-
def build_regions_for(coordinates)
|
|
49
|
-
coordinates
|
|
50
|
-
.map { |entry| Region.from_edge_coordinates(*entry) }
|
|
51
|
-
.tap { |region| region.compact! }
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
end
|
|
56
|
-
end
|
|
3
|
+
# Forwarder (ADR-004 v2 step 6): Capybara::Screenshot::Diff::AreaCalculator
|
|
4
|
+
# now resolves lazily via snap_diff/legacy_shims' const_missing, with a
|
|
5
|
+
# deprecation warning pointing at SnapDiff::AreaCalculator.
|
|
6
|
+
require "snap_diff/area_calculator"
|
|
7
|
+
require "snap_diff/legacy_shims"
|
|
@@ -1,121 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def self.resize_window_if_needed
|
|
9
|
-
if ::Capybara::Screenshot.respond_to?(:window_size) && ::Capybara::Screenshot.window_size
|
|
10
|
-
resize_to(::Capybara::Screenshot.window_size)
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def self.resize_to(window_size)
|
|
15
|
-
if session.driver.respond_to?(:resize)
|
|
16
|
-
session.driver.resize(*window_size)
|
|
17
|
-
elsif BrowserHelpers.selenium?
|
|
18
|
-
session.driver.browser.manage.window.resize_to(*window_size)
|
|
19
|
-
end
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def self.selenium?
|
|
23
|
-
current_capybara_driver_class <= Capybara::Selenium::Driver
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def self.window_size_is_wrong?(expected_window_size = nil)
|
|
27
|
-
selenium? && expected_window_size &&
|
|
28
|
-
session.driver.browser.manage.window.size != ::Selenium::WebDriver::Dimension.new(*expected_window_size)
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def self.bounds_for_css(*css_selectors)
|
|
32
|
-
css_selectors.reduce([]) do |regions, selector|
|
|
33
|
-
regions.concat(all_visible_regions_for(selector))
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
IMAGE_WAIT_SCRIPT = <<~JS
|
|
38
|
-
function pending_image() {
|
|
39
|
-
const images = document.images
|
|
40
|
-
for (var i = 0; i < images.length; i++) {
|
|
41
|
-
if (!images[i].complete && images[i].loading !== "lazy") {
|
|
42
|
-
return images[i].src
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return false
|
|
46
|
-
}(window)
|
|
47
|
-
JS
|
|
48
|
-
|
|
49
|
-
HIDE_CARET_SCRIPT = <<~JS
|
|
50
|
-
if (!document.getElementById('csdHideCaretStyle')) {
|
|
51
|
-
let style = document.createElement('style');
|
|
52
|
-
style.setAttribute('id', 'csdHideCaretStyle');
|
|
53
|
-
document.head.appendChild(style);
|
|
54
|
-
let styleSheet = style.sheet;
|
|
55
|
-
styleSheet.insertRule("* { caret-color: transparent !important; }", 0);
|
|
56
|
-
}
|
|
57
|
-
JS
|
|
58
|
-
|
|
59
|
-
def self.hide_caret
|
|
60
|
-
session.execute_script(HIDE_CARET_SCRIPT)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
DISABLE_ANIMATIONS_SCRIPT = <<~JS
|
|
64
|
-
if (!document.getElementById('csdDisableAnimationsStyle')) {
|
|
65
|
-
let style = document.createElement('style');
|
|
66
|
-
style.setAttribute('id', 'csdDisableAnimationsStyle');
|
|
67
|
-
style.textContent = '*, *::before, *::after { animation-duration: 0s !important; animation-delay: 0s !important; transition-duration: 0s !important; transition-delay: 0s !important; }';
|
|
68
|
-
document.head.appendChild(style);
|
|
69
|
-
}
|
|
70
|
-
JS
|
|
71
|
-
|
|
72
|
-
def self.disable_animations
|
|
73
|
-
session.execute_script(DISABLE_ANIMATIONS_SCRIPT)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
FIND_ACTIVE_ELEMENT_SCRIPT = <<~JS
|
|
77
|
-
function activeElement(){
|
|
78
|
-
const ae = document.activeElement;
|
|
79
|
-
if (ae.nodeName === "INPUT" || ae.nodeName === "TEXTAREA") {
|
|
80
|
-
ae.blur();
|
|
81
|
-
return ae;
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}(window);
|
|
85
|
-
JS
|
|
86
|
-
|
|
87
|
-
def self.blur_from_focused_element
|
|
88
|
-
session.evaluate_script(FIND_ACTIVE_ELEMENT_SCRIPT)
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
GET_BOUNDING_CLIENT_RECT_SCRIPT = <<~JS
|
|
92
|
-
[
|
|
93
|
-
this.getBoundingClientRect().left,
|
|
94
|
-
this.getBoundingClientRect().top,
|
|
95
|
-
this.getBoundingClientRect().right,
|
|
96
|
-
this.getBoundingClientRect().bottom
|
|
97
|
-
]
|
|
98
|
-
JS
|
|
99
|
-
|
|
100
|
-
def self.all_visible_regions_for(selector)
|
|
101
|
-
BrowserHelpers.session.all(selector, visible: true).map { |el| region_for(el) }
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def self.region_for(element)
|
|
105
|
-
element.evaluate_script(GET_BOUNDING_CLIENT_RECT_SCRIPT).map { |point| point.negative? ? 0 : point.ceil.to_i }
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def self.session
|
|
109
|
-
Capybara.current_session
|
|
110
|
-
end
|
|
111
|
-
|
|
112
|
-
def self.pending_image_to_load
|
|
113
|
-
BrowserHelpers.session.evaluate_script(IMAGE_WAIT_SCRIPT)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def self.current_capybara_driver_class
|
|
117
|
-
session.driver.class
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
3
|
+
# Forwarder (ADR-004 v2 step 6): Capybara::Screenshot::BrowserHelpers now
|
|
4
|
+
# resolves lazily via snap_diff/legacy_shims' const_missing, with a
|
|
5
|
+
# deprecation warning pointing at SnapDiff::BrowserHelpers.
|
|
6
|
+
require "snap_diff/browser_helpers"
|
|
7
|
+
require "snap_diff/legacy_shims"
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Legacy Capybara::Screenshot / Capybara::Screenshot::Diff mattr_accessor
|
|
4
|
+
# config module, extracted out of capybara_screenshot_diff.rb so it is a
|
|
5
|
+
# leaf: it can be required by both capybara_screenshot_diff.rb (the old
|
|
6
|
+
# umbrella entry point) and snap_diff.rb (the canonical entry point)
|
|
7
|
+
# without either of *those* requiring the other back.
|
|
8
|
+
#
|
|
9
|
+
# Why this file has to exist at all: several lib/snap_diff/* units
|
|
10
|
+
# (Screenshoter, SnapManager, Utils) are referenced here at class-body
|
|
11
|
+
# eval time (mattr_accessor default blocks, AVAILABLE_DRIVERS) rather
|
|
12
|
+
# than at call time, so they must be real, already-loaded classes before
|
|
13
|
+
# this module body runs. Since v2 step 6 they are pulled in and referenced
|
|
14
|
+
# under their canonical SnapDiff names: the old-name aliases are lazy
|
|
15
|
+
# const_missing shims that emit deprecation warnings, and the gem's own
|
|
16
|
+
# code must stay warning-free. Everything else this module references
|
|
17
|
+
# (Os, Comparison) is referenced only inside method bodies, resolved
|
|
18
|
+
# lazily at call time by whichever entry point loaded them.
|
|
19
|
+
require "snap_diff/screenshoter"
|
|
20
|
+
require "snap_diff/snap_manager"
|
|
21
|
+
require "snap_diff/utils"
|
|
22
|
+
|
|
23
|
+
module Capybara
|
|
24
|
+
module Screenshot
|
|
25
|
+
mattr_accessor :add_driver_path
|
|
26
|
+
mattr_accessor :add_os_path
|
|
27
|
+
mattr_accessor(:blur_active_element) { true }
|
|
28
|
+
mattr_accessor :enabled
|
|
29
|
+
mattr_accessor(:hide_caret) { true }
|
|
30
|
+
mattr_accessor :disable_animations
|
|
31
|
+
mattr_reader(:root) { (defined?(Rails) && defined?(Rails.root) && Rails.root) || Pathname(".").expand_path }
|
|
32
|
+
mattr_accessor :stability_time_limit
|
|
33
|
+
mattr_accessor :window_size
|
|
34
|
+
mattr_accessor(:save_path) { "doc/screenshots" }
|
|
35
|
+
mattr_accessor(:use_lfs)
|
|
36
|
+
mattr_accessor(:screenshot_format) { "png" }
|
|
37
|
+
mattr_accessor(:capybara_screenshot_options) { {} }
|
|
38
|
+
|
|
39
|
+
class << self
|
|
40
|
+
def root=(path)
|
|
41
|
+
@@root = Pathname(path).expand_path
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def active?
|
|
45
|
+
enabled || (enabled.nil? && Diff.enabled)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def screenshot_area
|
|
49
|
+
parts = [Screenshot.save_path]
|
|
50
|
+
parts << SnapDiff::Os.name if Screenshot.add_os_path
|
|
51
|
+
parts << Capybara.current_driver.to_s if Screenshot.add_driver_path
|
|
52
|
+
File.join(*parts)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def screenshot_area_abs
|
|
56
|
+
root / screenshot_area
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Module to track screenshot changes
|
|
61
|
+
module Diff
|
|
62
|
+
mattr_accessor(:delayed) { true }
|
|
63
|
+
mattr_accessor :area_size_limit
|
|
64
|
+
mattr_accessor(:fail_if_new) { !ENV["CI"].nil? && !ENV["CI"].empty? }
|
|
65
|
+
mattr_accessor(:pending_if_new) { false }
|
|
66
|
+
mattr_accessor(:fail_on_difference) { true }
|
|
67
|
+
mattr_accessor :color_distance_limit
|
|
68
|
+
mattr_accessor(:enabled) { true }
|
|
69
|
+
mattr_accessor :shift_distance_limit
|
|
70
|
+
mattr_accessor :skip_area
|
|
71
|
+
mattr_accessor(:driver) { :auto }
|
|
72
|
+
mattr_accessor :tolerance
|
|
73
|
+
mattr_accessor :perceptual_threshold
|
|
74
|
+
|
|
75
|
+
mattr_accessor(:screenshoter) { SnapDiff::Screenshoter }
|
|
76
|
+
mattr_accessor(:manager) { SnapDiff::SnapManager }
|
|
77
|
+
|
|
78
|
+
AVAILABLE_DRIVERS = SnapDiff::Utils.detect_available_drivers.freeze
|
|
79
|
+
|
|
80
|
+
# Configure screenshot and diff settings in one block.
|
|
81
|
+
#
|
|
82
|
+
# Capybara::Screenshot::Diff.configure do |screenshot, diff|
|
|
83
|
+
# screenshot.window_size = [1280, 1024]
|
|
84
|
+
# screenshot.stability_time_limit = 1
|
|
85
|
+
# diff.driver = :vips
|
|
86
|
+
# diff.tolerance = 0.0005
|
|
87
|
+
# end
|
|
88
|
+
def self.configure
|
|
89
|
+
yield Screenshot, self
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.compare(baseline_path, current_path, **options)
|
|
93
|
+
SnapDiff::Comparison.new(current_path, baseline_path, default_options.merge(options))
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def self.default_options
|
|
97
|
+
{
|
|
98
|
+
area_size_limit: area_size_limit,
|
|
99
|
+
color_distance_limit: color_distance_limit,
|
|
100
|
+
driver: driver,
|
|
101
|
+
screenshot_format: Screenshot.screenshot_format,
|
|
102
|
+
capybara_screenshot_options: Screenshot.capybara_screenshot_options,
|
|
103
|
+
perceptual_threshold: perceptual_threshold,
|
|
104
|
+
shift_distance_limit: shift_distance_limit,
|
|
105
|
+
skip_area: skip_area,
|
|
106
|
+
stability_time_limit: Screenshot.stability_time_limit,
|
|
107
|
+
tolerance: tolerance || ((driver == :vips) ? 0.001 : nil),
|
|
108
|
+
wait: Capybara.default_max_wait_time
|
|
109
|
+
}
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
@@ -1,104 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
#
|
|
10
|
-
# This value object encapsulates the result of an image comparison operation.
|
|
11
|
-
# It follows the Single Responsibility Principle by focusing solely on representing
|
|
12
|
-
# the difference state, including:
|
|
13
|
-
# - Whether images are different or equal
|
|
14
|
-
# - Why they differ (dimensions, pixels, etc.)
|
|
15
|
-
# - The specific region of difference
|
|
16
|
-
# - Whether differences are tolerable based on configured thresholds
|
|
17
|
-
#
|
|
18
|
-
# As part of the layered comparison architecture, this class represents the final
|
|
19
|
-
# output of the comparison process, containing all data needed for reporting.
|
|
20
|
-
# Represents a difference between two images
|
|
21
|
-
class Difference < Struct.new(:region, :meta, :comparison, :failed_by, :base_image_path, :image_path, keyword_init: nil)
|
|
22
|
-
def self.build_null(comparison, base_image_path, new_image_path, failed_by = nil)
|
|
23
|
-
Difference.new(
|
|
24
|
-
nil,
|
|
25
|
-
{difference_level: nil, max_color_distance: 0},
|
|
26
|
-
comparison,
|
|
27
|
-
failed_by,
|
|
28
|
-
base_image_path,
|
|
29
|
-
new_image_path
|
|
30
|
-
).freeze
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def different?
|
|
34
|
-
failed? || !(blank? || tolerable?)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def equal?
|
|
38
|
-
!different?
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def failed?
|
|
42
|
-
!!failed_by
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def options
|
|
46
|
-
comparison.options
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def tolerance
|
|
50
|
-
options[:tolerance]
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
def skip_area
|
|
54
|
-
comparison.skip_area
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def area_size_limit
|
|
58
|
-
options[:area_size_limit]
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def blank?
|
|
62
|
-
region.nil? || region_area_size.zero?
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
def region_area_size
|
|
66
|
-
@region_area_size ||= region&.size || 0
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def ratio
|
|
70
|
-
meta[:difference_level]
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
def to_h
|
|
74
|
-
{area_size: region_area_size, region: coordinates}.merge!(meta)
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def coordinates
|
|
78
|
-
region&.to_edge_coordinates
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
def inspect
|
|
82
|
-
to_h.to_json
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def tolerable?
|
|
86
|
-
!!((area_size_limit && area_size_limit >= region_area_size) || (tolerance && tolerance >= ratio))
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
# Path accessors for backward compatibility
|
|
90
|
-
def new_image_path
|
|
91
|
-
image_path || comparison&.new_image_path
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
def original_image_path
|
|
95
|
-
base_image_path || comparison&.base_image_path
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def diff_mask
|
|
99
|
-
meta[:diff_mask]
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
end
|
|
3
|
+
# Forwarder (ADR-004 v2 step 6): the comparison-result value object lives at
|
|
4
|
+
# SnapDiff::ComparisonResult (ex-Difference); the old name now resolves
|
|
5
|
+
# lazily via snap_diff/legacy_shims' const_missing, with a deprecation
|
|
6
|
+
# warning.
|
|
7
|
+
require "snap_diff/comparison_result"
|
|
8
|
+
require "snap_diff/legacy_shims"
|
|
@@ -1,41 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
module Drivers
|
|
11
|
-
class BaseDriver
|
|
12
|
-
PNG_EXTENSION = ".png"
|
|
13
|
-
|
|
14
|
-
def same_dimension?(comparison)
|
|
15
|
-
dimension(comparison.base_image) == dimension(comparison.new_image)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def height_for(image)
|
|
19
|
-
image.height
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def width_for(image)
|
|
23
|
-
image.width
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def image_area_size(image)
|
|
27
|
-
width_for(image) * height_for(image)
|
|
28
|
-
end
|
|
29
|
-
|
|
30
|
-
def dimension(image)
|
|
31
|
-
[width_for(image), height_for(image)]
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def supports?(feature)
|
|
35
|
-
respond_to?(feature)
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|
|
3
|
+
# BaseDriver was dissolved into the SnapDiff::Driver mixin (ADR-004 v2
|
|
4
|
+
# step 4); since step 6 the old name resolves lazily via
|
|
5
|
+
# snap_diff/legacy_shims' const_missing, with a deprecation warning.
|
|
6
|
+
# Note it is now a module -- `class MyDriver < BaseDriver` becomes
|
|
7
|
+
# `include SnapDiff::Driver`.
|
|
8
|
+
require "snap_diff/driver"
|
|
9
|
+
require "snap_diff/legacy_shims"
|