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
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
4
|
-
require "capybara_screenshot_diff/minitest"
|
|
3
|
+
require "snap_diff/static"
|
|
5
4
|
|
|
6
5
|
module CapybaraScreenshotDiff
|
|
7
|
-
def self.serve(
|
|
8
|
-
|
|
9
|
-
Capybara::Screenshot.root = root
|
|
6
|
+
def self.serve(...)
|
|
7
|
+
SnapDiff.serve(...)
|
|
10
8
|
end
|
|
11
9
|
end
|
|
@@ -1,121 +1,50 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
# Lazy load: snap_diff.rb requires this file, so eagerly `require`-ing it
|
|
4
|
-
# here would be circular. `autoload` defers the load until SnapDiff is
|
|
5
|
-
# first referenced, by which point this file has already finished loading.
|
|
6
|
-
autoload :SnapDiff, "snap_diff"
|
|
7
|
-
|
|
8
3
|
require "capybara/dsl"
|
|
4
|
+
require "capybara/screenshot/diff/config_legacy"
|
|
9
5
|
require "capybara/screenshot/diff/version"
|
|
6
|
+
require "capybara/screenshot/diff/os"
|
|
7
|
+
require "capybara/screenshot/diff/browser_helpers"
|
|
10
8
|
require "capybara/screenshot/diff/utils"
|
|
11
9
|
require "capybara/screenshot/diff/image_compare"
|
|
12
10
|
require "capybara_screenshot_diff/snap_manager"
|
|
11
|
+
require "capybara_screenshot_diff/snap"
|
|
13
12
|
require "capybara/screenshot/diff/screenshoter"
|
|
13
|
+
require "capybara/screenshot/diff/stable_screenshoter"
|
|
14
|
+
require "capybara/screenshot/diff/vcs"
|
|
15
|
+
require "capybara/screenshot/diff/area_calculator"
|
|
16
|
+
require "capybara/screenshot/diff/image_preprocessor"
|
|
17
|
+
require "capybara/screenshot/diff/annotation_service"
|
|
18
|
+
require "capybara_screenshot_diff/screenshot_namer"
|
|
19
|
+
require "capybara_screenshot_diff/screenshot_assertion"
|
|
20
|
+
require "capybara_screenshot_diff/attempts_reporter"
|
|
21
|
+
require "capybara/screenshot/diff/screenshot_matcher"
|
|
14
22
|
require "capybara/screenshot/diff/reporters/default"
|
|
15
23
|
|
|
16
24
|
require "capybara_screenshot_diff/error_with_filtered_backtrace"
|
|
17
25
|
|
|
18
26
|
module CapybaraScreenshotDiff
|
|
19
|
-
RED_RGBA
|
|
20
|
-
|
|
27
|
+
# RED_RGBA / ORANGE_RGBA moved to SnapDiff (snap_diff/annotation_service)
|
|
28
|
+
# so the bare "snap_diff" entry gets them too; the old names resolve via
|
|
29
|
+
# snap_diff/legacy_shims with a deprecation warning.
|
|
21
30
|
|
|
22
|
-
class CapybaraScreenshotDiffError < ErrorWithFilteredBacktrace; end
|
|
31
|
+
class CapybaraScreenshotDiffError < SnapDiff::ErrorWithFilteredBacktrace; end
|
|
23
32
|
|
|
24
33
|
class ExpectationNotMet < CapybaraScreenshotDiffError; end
|
|
25
34
|
|
|
26
35
|
class UnstableImage < CapybaraScreenshotDiffError; end
|
|
27
36
|
|
|
28
|
-
class WindowSizeMismatchError < ErrorWithFilteredBacktrace; end
|
|
37
|
+
class WindowSizeMismatchError < SnapDiff::ErrorWithFilteredBacktrace; end
|
|
29
38
|
end
|
|
30
39
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
mattr_accessor(:save_path) { "doc/screenshots" }
|
|
43
|
-
mattr_accessor(:use_lfs)
|
|
44
|
-
mattr_accessor(:screenshot_format) { "png" }
|
|
45
|
-
mattr_accessor(:capybara_screenshot_options) { {} }
|
|
46
|
-
|
|
47
|
-
class << self
|
|
48
|
-
def root=(path)
|
|
49
|
-
@@root = Pathname(path).expand_path
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def active?
|
|
53
|
-
enabled || (enabled.nil? && Diff.enabled)
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def screenshot_area
|
|
57
|
-
parts = [Screenshot.save_path]
|
|
58
|
-
parts << Os.name if Screenshot.add_os_path
|
|
59
|
-
parts << Capybara.current_driver.to_s if Screenshot.add_driver_path
|
|
60
|
-
File.join(*parts)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
def screenshot_area_abs
|
|
64
|
-
root / screenshot_area
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
# Module to track screenshot changes
|
|
69
|
-
module Diff
|
|
70
|
-
mattr_accessor(:delayed) { true }
|
|
71
|
-
mattr_accessor :area_size_limit
|
|
72
|
-
mattr_accessor(:fail_if_new) { !ENV["CI"].nil? && !ENV["CI"].empty? }
|
|
73
|
-
mattr_accessor(:pending_if_new) { false }
|
|
74
|
-
mattr_accessor(:fail_on_difference) { true }
|
|
75
|
-
mattr_accessor :color_distance_limit
|
|
76
|
-
mattr_accessor(:enabled) { true }
|
|
77
|
-
mattr_accessor :shift_distance_limit
|
|
78
|
-
mattr_accessor :skip_area
|
|
79
|
-
mattr_accessor(:driver) { :auto }
|
|
80
|
-
mattr_accessor :tolerance
|
|
81
|
-
mattr_accessor :perceptual_threshold
|
|
82
|
-
|
|
83
|
-
mattr_accessor(:screenshoter) { Screenshoter }
|
|
84
|
-
mattr_accessor(:manager) { CapybaraScreenshotDiff::SnapManager }
|
|
85
|
-
|
|
86
|
-
AVAILABLE_DRIVERS = Utils.detect_available_drivers.freeze
|
|
87
|
-
|
|
88
|
-
# Configure screenshot and diff settings in one block.
|
|
89
|
-
#
|
|
90
|
-
# Capybara::Screenshot::Diff.configure do |screenshot, diff|
|
|
91
|
-
# screenshot.window_size = [1280, 1024]
|
|
92
|
-
# screenshot.stability_time_limit = 1
|
|
93
|
-
# diff.driver = :vips
|
|
94
|
-
# diff.tolerance = 0.0005
|
|
95
|
-
# end
|
|
96
|
-
def self.configure
|
|
97
|
-
yield Screenshot, self
|
|
98
|
-
end
|
|
99
|
-
|
|
100
|
-
def self.compare(baseline_path, current_path, **options)
|
|
101
|
-
ImageCompare.new(current_path, baseline_path, default_options.merge(options))
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def self.default_options
|
|
105
|
-
{
|
|
106
|
-
area_size_limit: area_size_limit,
|
|
107
|
-
color_distance_limit: color_distance_limit,
|
|
108
|
-
driver: driver,
|
|
109
|
-
screenshot_format: Screenshot.screenshot_format,
|
|
110
|
-
capybara_screenshot_options: Screenshot.capybara_screenshot_options,
|
|
111
|
-
perceptual_threshold: perceptual_threshold,
|
|
112
|
-
shift_distance_limit: shift_distance_limit,
|
|
113
|
-
skip_area: skip_area,
|
|
114
|
-
stability_time_limit: Screenshot.stability_time_limit,
|
|
115
|
-
tolerance: tolerance || ((driver == :vips) ? 0.001 : nil),
|
|
116
|
-
wait: Capybara.default_max_wait_time
|
|
117
|
-
}
|
|
118
|
-
end
|
|
119
|
-
end
|
|
120
|
-
end
|
|
121
|
-
end
|
|
40
|
+
require "capybara_screenshot_diff/dsl"
|
|
41
|
+
|
|
42
|
+
# Eager, not autoload: several lib/snap_diff/* units above (Os,
|
|
43
|
+
# Screenshoter, ...) reopen `module SnapDiff` while loading, which cancels
|
|
44
|
+
# any registered `autoload :SnapDiff` before it ever fires (Ruby resolves
|
|
45
|
+
# the constant the first time anything reopens it, autoload or not) --
|
|
46
|
+
# so SnapDiff.start/.compare/.config would silently never be defined
|
|
47
|
+
# without this. Safe to do eagerly, unlike before this file's units
|
|
48
|
+
# moved: snap_diff.rb no longer requires this file back (it only needs
|
|
49
|
+
# the leaf config_legacy.rb + image_compare.rb), so there is no cycle.
|
|
50
|
+
require "snap_diff"
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SnapDiff
|
|
4
|
+
# Annotation colors, defined here (not in the capybara_screenshot_diff
|
|
5
|
+
# umbrella) so they resolve in processes that only `require "snap_diff"`.
|
|
6
|
+
RED_RGBA = [255, 0, 0, 255].freeze
|
|
7
|
+
ORANGE_RGBA = [255, 192, 0, 255].freeze
|
|
8
|
+
|
|
9
|
+
# Draws diff/skip-area rectangles and the heatmap overlay for a Difference,
|
|
10
|
+
# and saves the resulting images to their `.diff.*` / `.heatmap.diff.*` paths.
|
|
11
|
+
#
|
|
12
|
+
# Extracted from Reporters::Default (ADR-004 PR 7) so the reporter only
|
|
13
|
+
# builds error messages; this class owns all image annotation work.
|
|
14
|
+
class AnnotationService
|
|
15
|
+
attr_reader :annotated_image_path, :annotated_base_image_path, :heatmap_diff_path
|
|
16
|
+
|
|
17
|
+
def initialize(difference)
|
|
18
|
+
@difference = difference
|
|
19
|
+
|
|
20
|
+
comparison = difference.comparison
|
|
21
|
+
ext = comparison.new_image_path.extname.delete_prefix(".")
|
|
22
|
+
screenshot_format = comparison.options[:screenshot_format] || (ext unless ext.empty?) || "png"
|
|
23
|
+
@annotated_image_path = comparison.new_image_path.sub_ext(".diff.#{screenshot_format}")
|
|
24
|
+
@annotated_base_image_path = comparison.base_image_path.sub_ext(".diff.#{screenshot_format}")
|
|
25
|
+
@heatmap_diff_path = comparison.new_image_path.sub_ext(".heatmap.diff.#{screenshot_format}")
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def clean_tmp_files
|
|
29
|
+
annotated_base_image_path.unlink if annotated_base_image_path.exist?
|
|
30
|
+
annotated_image_path.unlink if annotated_image_path.exist?
|
|
31
|
+
heatmap_diff_path.unlink if heatmap_diff_path.exist?
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def annotate_and_save_images
|
|
35
|
+
save_annotation_for(new_image, annotated_image_path)
|
|
36
|
+
save_annotation_for(base_image, annotated_base_image_path)
|
|
37
|
+
save_heatmap_diff if difference.diff_mask
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def save_annotation_for(image, image_path)
|
|
41
|
+
image = annotate_difference(image, difference.region)
|
|
42
|
+
image = annotate_skip_areas(image, difference.comparison.skip_area) if difference.comparison.skip_area
|
|
43
|
+
|
|
44
|
+
save(image, image_path.to_path)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def annotate_difference(image, region)
|
|
48
|
+
driver.draw_rectangles([image], region, RED_RGBA, offset: 1).first
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def annotate_skip_areas(image, skip_areas)
|
|
52
|
+
skip_areas.reduce(image) do |memo, region|
|
|
53
|
+
driver.draw_rectangles([memo], region, ORANGE_RGBA).first
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def save(image, image_path)
|
|
58
|
+
driver.save_image_to(image, image_path.to_s)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
attr_reader :difference
|
|
64
|
+
|
|
65
|
+
def save_heatmap_diff
|
|
66
|
+
merged_image = driver.merge(new_image, base_image)
|
|
67
|
+
highlighted_mask = driver.highlight_mask(difference.diff_mask, merged_image, color: RED_RGBA)
|
|
68
|
+
|
|
69
|
+
save(highlighted_mask, heatmap_diff_path.to_path)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def base_image
|
|
73
|
+
difference.comparison.base_image
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def new_image
|
|
77
|
+
difference.comparison.new_image
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def driver
|
|
81
|
+
@_driver ||= difference.comparison.driver
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Region has not moved yet (ADR-004 v2 scoping: it's already unnamespaced,
|
|
4
|
+
# no v2.0 move needed).
|
|
5
|
+
require "capybara/screenshot/diff/region"
|
|
6
|
+
|
|
7
|
+
module SnapDiff
|
|
8
|
+
class AreaCalculator
|
|
9
|
+
def initialize(crop_coordinates, skip_area)
|
|
10
|
+
@crop_coordinates = crop_coordinates
|
|
11
|
+
@skip_area = skip_area
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def calculate_crop
|
|
15
|
+
return @_calculated_crop if defined?(@_calculated_crop)
|
|
16
|
+
return @_calculated_crop = nil unless @crop_coordinates
|
|
17
|
+
|
|
18
|
+
# TODO: Move out from this class, this should be done on before screenshot and should not depend on Browser
|
|
19
|
+
@crop_coordinates = BrowserHelpers.bounds_for_css(@crop_coordinates).first if @crop_coordinates.is_a?(String)
|
|
20
|
+
@_calculated_crop = Region.from_edge_coordinates(*@crop_coordinates)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Cast skip areas params into Region
|
|
24
|
+
# and if there is crop then makes absolute coordinates to eb relative to crop top left corner
|
|
25
|
+
def calculate_skip_area
|
|
26
|
+
return nil unless @skip_area
|
|
27
|
+
|
|
28
|
+
crop_region = calculate_crop
|
|
29
|
+
skip_area = Array(@skip_area)
|
|
30
|
+
|
|
31
|
+
css_selectors, coords_list = skip_area.compact.partition { |region| region.is_a? String }
|
|
32
|
+
regions, coords_list = coords_list.partition { |region| region.is_a? Region }
|
|
33
|
+
|
|
34
|
+
regions.concat(build_regions_for(BrowserHelpers.bounds_for_css(*css_selectors))) unless css_selectors.empty?
|
|
35
|
+
regions.concat(build_regions_for(coords_list.flatten.each_slice(4))) unless coords_list.empty?
|
|
36
|
+
|
|
37
|
+
regions.compact!
|
|
38
|
+
|
|
39
|
+
if crop_region
|
|
40
|
+
regions
|
|
41
|
+
.map! { |region| crop_region.find_relative_intersect(region) }
|
|
42
|
+
.filter! { |region| region&.present? }
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
regions
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def build_regions_for(coordinates)
|
|
51
|
+
coordinates
|
|
52
|
+
.map { |entry| Region.from_edge_coordinates(*entry) }
|
|
53
|
+
.tap { |region| region.compact! }
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
|
|
5
|
+
require "snap_diff/comparison"
|
|
6
|
+
|
|
7
|
+
module SnapDiff
|
|
8
|
+
class AttemptsReporter
|
|
9
|
+
def initialize(snapshot, comparison_options, stability_options = {})
|
|
10
|
+
@snapshot = snapshot
|
|
11
|
+
@comparison_options = comparison_options
|
|
12
|
+
@wait = stability_options[:wait]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def generate
|
|
16
|
+
attempts_screenshot_paths = @snapshot.find_attempts_paths
|
|
17
|
+
|
|
18
|
+
annotate_attempts(attempts_screenshot_paths)
|
|
19
|
+
|
|
20
|
+
"Could not get stable screenshot within #{@wait}s:\n#{attempts_screenshot_paths.join("\n")}"
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def build_comparison_for(attempt_path, previous_attempt_path)
|
|
24
|
+
Comparison.new(attempt_path, previous_attempt_path, @comparison_options)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def annotate_attempts(attempts_screenshot_paths)
|
|
30
|
+
previous_file = nil
|
|
31
|
+
attempts_screenshot_paths.reverse_each do |file_name|
|
|
32
|
+
if previous_file && File.exist?(previous_file)
|
|
33
|
+
attempts_comparison = build_comparison_for(file_name, previous_file)
|
|
34
|
+
|
|
35
|
+
if attempts_comparison.different?
|
|
36
|
+
FileUtils.mv(attempts_comparison.reporter.annotated_base_image_path, previous_file, force: true)
|
|
37
|
+
else
|
|
38
|
+
warn "[capybara-screenshot-diff] Some attempts was stable, but mistakenly marked as not: " \
|
|
39
|
+
"#{previous_file} and #{file_name} are equal"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
FileUtils.rm(attempts_comparison.reporter.annotated_image_path, force: true)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
previous_file = file_name
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
previous_file
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Region has not moved yet (ADR-004 v2 scoping: it's already unnamespaced,
|
|
4
|
+
# no v2.0 move needed) so this is an absolute require, not require_relative.
|
|
5
|
+
require "capybara/screenshot/diff/region"
|
|
6
|
+
|
|
7
|
+
module SnapDiff
|
|
8
|
+
module BrowserHelpers
|
|
9
|
+
def self.resize_window_if_needed
|
|
10
|
+
if ::Capybara::Screenshot.respond_to?(:window_size) && ::Capybara::Screenshot.window_size
|
|
11
|
+
resize_to(::Capybara::Screenshot.window_size)
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def self.resize_to(window_size)
|
|
16
|
+
if session.driver.respond_to?(:resize)
|
|
17
|
+
session.driver.resize(*window_size)
|
|
18
|
+
elsif BrowserHelpers.selenium?
|
|
19
|
+
session.driver.browser.manage.window.resize_to(*window_size)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.selenium?
|
|
24
|
+
current_capybara_driver_class <= Capybara::Selenium::Driver
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.window_size_is_wrong?(expected_window_size = nil)
|
|
28
|
+
selenium? && expected_window_size &&
|
|
29
|
+
session.driver.browser.manage.window.size != ::Selenium::WebDriver::Dimension.new(*expected_window_size)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.bounds_for_css(*css_selectors)
|
|
33
|
+
css_selectors.reduce([]) do |regions, selector|
|
|
34
|
+
regions.concat(all_visible_regions_for(selector))
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
IMAGE_WAIT_SCRIPT = <<~JS
|
|
39
|
+
function pending_image() {
|
|
40
|
+
const images = document.images
|
|
41
|
+
for (var i = 0; i < images.length; i++) {
|
|
42
|
+
if (!images[i].complete && images[i].loading !== "lazy") {
|
|
43
|
+
return images[i].src
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return false
|
|
47
|
+
}(window)
|
|
48
|
+
JS
|
|
49
|
+
|
|
50
|
+
HIDE_CARET_SCRIPT = <<~JS
|
|
51
|
+
if (!document.getElementById('csdHideCaretStyle')) {
|
|
52
|
+
let style = document.createElement('style');
|
|
53
|
+
style.setAttribute('id', 'csdHideCaretStyle');
|
|
54
|
+
document.head.appendChild(style);
|
|
55
|
+
let styleSheet = style.sheet;
|
|
56
|
+
styleSheet.insertRule("* { caret-color: transparent !important; }", 0);
|
|
57
|
+
}
|
|
58
|
+
JS
|
|
59
|
+
|
|
60
|
+
def self.hide_caret
|
|
61
|
+
session.execute_script(HIDE_CARET_SCRIPT)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
DISABLE_ANIMATIONS_SCRIPT = <<~JS
|
|
65
|
+
if (!document.getElementById('csdDisableAnimationsStyle')) {
|
|
66
|
+
let style = document.createElement('style');
|
|
67
|
+
style.setAttribute('id', 'csdDisableAnimationsStyle');
|
|
68
|
+
style.textContent = '*, *::before, *::after { animation-duration: 0s !important; animation-delay: 0s !important; transition-duration: 0s !important; transition-delay: 0s !important; }';
|
|
69
|
+
document.head.appendChild(style);
|
|
70
|
+
}
|
|
71
|
+
JS
|
|
72
|
+
|
|
73
|
+
def self.disable_animations
|
|
74
|
+
session.execute_script(DISABLE_ANIMATIONS_SCRIPT)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
FIND_ACTIVE_ELEMENT_SCRIPT = <<~JS
|
|
78
|
+
function activeElement(){
|
|
79
|
+
const ae = document.activeElement;
|
|
80
|
+
if (ae.nodeName === "INPUT" || ae.nodeName === "TEXTAREA") {
|
|
81
|
+
ae.blur();
|
|
82
|
+
return ae;
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
}(window);
|
|
86
|
+
JS
|
|
87
|
+
|
|
88
|
+
def self.blur_from_focused_element
|
|
89
|
+
session.evaluate_script(FIND_ACTIVE_ELEMENT_SCRIPT)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
GET_BOUNDING_CLIENT_RECT_SCRIPT = <<~JS
|
|
93
|
+
[
|
|
94
|
+
this.getBoundingClientRect().left,
|
|
95
|
+
this.getBoundingClientRect().top,
|
|
96
|
+
this.getBoundingClientRect().right,
|
|
97
|
+
this.getBoundingClientRect().bottom
|
|
98
|
+
]
|
|
99
|
+
JS
|
|
100
|
+
|
|
101
|
+
def self.all_visible_regions_for(selector)
|
|
102
|
+
BrowserHelpers.session.all(selector, visible: true).map { |el| region_for(el) }
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def self.region_for(element)
|
|
106
|
+
element.evaluate_script(GET_BOUNDING_CLIENT_RECT_SCRIPT).map { |point| point.negative? ? 0 : point.ceil.to_i }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def self.session
|
|
110
|
+
Capybara.current_session
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def self.pending_image_to_load
|
|
114
|
+
BrowserHelpers.session.evaluate_script(IMAGE_WAIT_SCRIPT)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def self.current_capybara_driver_class
|
|
118
|
+
session.driver.class
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../browser_helpers"
|
|
4
|
+
|
|
5
|
+
module SnapDiff
|
|
6
|
+
module Capture
|
|
7
|
+
# Per-capture viewport preparation seam.
|
|
8
|
+
#
|
|
9
|
+
# Called exactly once per capture, before the screenshoter runs and outside
|
|
10
|
+
# any stability retry loop. Today it only validates the window size
|
|
11
|
+
# (raise-only, never resizes) — the same guard ScreenshotMatcher carried
|
|
12
|
+
# inline before. v3 hangs scroll-position preservation and element-anchored
|
|
13
|
+
# capture off this seam via the +anchor+ parameter without touching callers.
|
|
14
|
+
module Viewport
|
|
15
|
+
module_function
|
|
16
|
+
|
|
17
|
+
# @param expected_window_size [Array(Integer, Integer), nil] the configured window size
|
|
18
|
+
# @param anchor [Object, nil] reserved for v3 (scroll preservation /
|
|
19
|
+
# element-anchored capture); accepted but unused today.
|
|
20
|
+
# @raise [CapybaraScreenshotDiff::WindowSizeMismatchError] if the browser
|
|
21
|
+
# window does not match the expected size.
|
|
22
|
+
def prepare!(expected_window_size, anchor: nil)
|
|
23
|
+
return unless BrowserHelpers.window_size_is_wrong?(expected_window_size)
|
|
24
|
+
|
|
25
|
+
current_size = BrowserHelpers.selenium? ?
|
|
26
|
+
BrowserHelpers.session.driver.browser.manage.window.size.to_s :
|
|
27
|
+
"unknown"
|
|
28
|
+
|
|
29
|
+
raise CapybaraScreenshotDiff::WindowSizeMismatchError.new(<<~ERROR.chomp, caller)
|
|
30
|
+
Window size mismatch detected!
|
|
31
|
+
Expected: #{expected_window_size.inspect}
|
|
32
|
+
Actual: #{current_size}
|
|
33
|
+
|
|
34
|
+
Screenshots cannot be compared when window sizes don't match.
|
|
35
|
+
Please ensure the browser window is properly sized before taking screenshots.
|
|
36
|
+
ERROR
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|