capybara-screenshot-diff 1.8.0 → 1.10.2
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/Rakefile +1 -11
- data/capybara-screenshot-diff.gemspec +3 -4
- data/gems.rb +11 -8
- data/lib/capybara/screenshot/diff/area_calculator.rb +56 -0
- data/lib/capybara/screenshot/diff/browser_helpers.rb +5 -5
- data/lib/capybara/screenshot/diff/comparison.rb +6 -0
- data/lib/capybara/screenshot/diff/cucumber.rb +1 -9
- data/lib/capybara/screenshot/diff/difference.rb +8 -4
- data/lib/capybara/screenshot/diff/drivers/base_driver.rb +0 -5
- data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +10 -5
- data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +15 -4
- data/lib/capybara/screenshot/diff/drivers.rb +1 -1
- data/lib/capybara/screenshot/diff/image_compare.rb +84 -114
- data/lib/capybara/screenshot/diff/region.rb +28 -7
- data/lib/capybara/screenshot/diff/reporters/default.rb +121 -0
- data/lib/capybara/screenshot/diff/screenshot_matcher.rb +36 -74
- data/lib/capybara/screenshot/diff/screenshoter.rb +47 -56
- data/lib/capybara/screenshot/diff/stable_screenshoter.rb +65 -63
- data/lib/capybara/screenshot/diff/test_methods.rb +81 -13
- data/lib/capybara/screenshot/diff/{drivers/utils.rb → utils.rb} +2 -7
- data/lib/capybara/screenshot/diff/vcs.rb +26 -20
- data/lib/capybara/screenshot/diff/version.rb +1 -1
- data/lib/capybara/screenshot/diff.rb +1 -111
- data/lib/capybara-screenshot-diff.rb +1 -1
- data/lib/capybara_screenshot_diff/attempts_reporter.rb +49 -0
- data/lib/capybara_screenshot_diff/cucumber.rb +12 -0
- data/lib/capybara_screenshot_diff/dsl.rb +11 -0
- data/lib/capybara_screenshot_diff/minitest.rb +49 -0
- data/lib/capybara_screenshot_diff/rspec.rb +32 -0
- data/lib/capybara_screenshot_diff/snap.rb +55 -0
- data/lib/capybara_screenshot_diff/snap_manager.rb +76 -0
- data/lib/capybara_screenshot_diff.rb +86 -0
- metadata +20 -48
- data/lib/capybara/screenshot/diff/stabilization.rb +0 -0
- data/sig/capybara/screenshot/diff/diff.rbs +0 -28
- data/sig/capybara/screenshot/diff/difference.rbs +0 -33
- data/sig/capybara/screenshot/diff/drivers/base_driver.rbs +0 -63
- data/sig/capybara/screenshot/diff/drivers/browser_helpers.rbs +0 -36
- data/sig/capybara/screenshot/diff/drivers/chunky_png_driver.rbs +0 -89
- data/sig/capybara/screenshot/diff/drivers/utils.rbs +0 -13
- data/sig/capybara/screenshot/diff/drivers/vips_driver.rbs +0 -25
- data/sig/capybara/screenshot/diff/image_compare.rbs +0 -93
- data/sig/capybara/screenshot/diff/os.rbs +0 -11
- data/sig/capybara/screenshot/diff/region.rbs +0 -43
- data/sig/capybara/screenshot/diff/screenshot_matcher.rbs +0 -60
- data/sig/capybara/screenshot/diff/screenshoter.rbs +0 -48
- data/sig/capybara/screenshot/diff/stable_screenshoter.rbs +0 -29
- data/sig/capybara/screenshot/diff/test_methods.rbs +0 -39
- data/sig/capybara/screenshot/diff/vcs.rbs +0 -17
@@ -7,13 +7,6 @@ class Region
|
|
7
7
|
@x, @y, @width, @height = x, y, width, height
|
8
8
|
end
|
9
9
|
|
10
|
-
def self.from_top_left_corner_coordinates(x, y, width, height)
|
11
|
-
return nil unless x && y && width && height
|
12
|
-
return nil if width < 0 || height < 0
|
13
|
-
|
14
|
-
Region.new(x, y, width, height)
|
15
|
-
end
|
16
|
-
|
17
10
|
def self.from_edge_coordinates(left, top, right, bottom)
|
18
11
|
return nil unless left && top && right && bottom
|
19
12
|
return nil if right < left || bottom < top
|
@@ -83,4 +76,32 @@ class Region
|
|
83
76
|
def cover?(x, y)
|
84
77
|
left <= x && x <= right && top <= y && y <= bottom
|
85
78
|
end
|
79
|
+
|
80
|
+
def empty?
|
81
|
+
width.zero? || height.zero?
|
82
|
+
end
|
83
|
+
|
84
|
+
def blank?
|
85
|
+
empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
def present?
|
89
|
+
!empty?
|
90
|
+
end
|
91
|
+
|
92
|
+
def inspect
|
93
|
+
"Region(x: #{x}, y: #{y}, width: #{width}, height: #{height})"
|
94
|
+
end
|
95
|
+
|
96
|
+
# need to add this method to make it work with assert_equal
|
97
|
+
def ==(other)
|
98
|
+
case other
|
99
|
+
when Region
|
100
|
+
x == other.x && y == other.y && width == other.width && height == other.height
|
101
|
+
when Array
|
102
|
+
to_a == other
|
103
|
+
else
|
104
|
+
false
|
105
|
+
end
|
106
|
+
end
|
86
107
|
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Capybara::Screenshot::Diff
|
4
|
+
module Reporters
|
5
|
+
class Default
|
6
|
+
attr_reader :annotated_image_path, :annotated_base_image_path, :heatmap_diff_path, :difference
|
7
|
+
|
8
|
+
def initialize(difference)
|
9
|
+
@difference = difference
|
10
|
+
|
11
|
+
screenshot_format = difference.comparison.options[:screenshot_format] || comparison.new_image_path.extname.slice(1..-1)
|
12
|
+
@annotated_image_path = comparison.new_image_path.sub_ext(".diff.#{screenshot_format}")
|
13
|
+
@annotated_base_image_path = comparison.base_image_path.sub_ext(".diff.#{screenshot_format}")
|
14
|
+
@heatmap_diff_path = comparison.new_image_path.sub_ext(".heatmap.diff.#{screenshot_format}")
|
15
|
+
end
|
16
|
+
|
17
|
+
def generate
|
18
|
+
if difference.equal?
|
19
|
+
# NOTE: Delete previous run runtime files
|
20
|
+
clean_tmp_files
|
21
|
+
return nil
|
22
|
+
end
|
23
|
+
|
24
|
+
if difference.failed? && difference.failed_by[:different_dimensions]
|
25
|
+
return build_error_for_different_dimensions
|
26
|
+
end
|
27
|
+
|
28
|
+
annotate_and_save_images
|
29
|
+
build_error_message
|
30
|
+
end
|
31
|
+
|
32
|
+
def clean_tmp_files
|
33
|
+
annotated_base_image_path.unlink if annotated_base_image_path.exist?
|
34
|
+
annotated_image_path.unlink if annotated_image_path.exist?
|
35
|
+
end
|
36
|
+
|
37
|
+
def build_error_for_different_dimensions
|
38
|
+
change_msg = [comparison.base_image, comparison.new_image]
|
39
|
+
.map { |image| driver.dimension(image).join("x") }
|
40
|
+
.join(" => ")
|
41
|
+
|
42
|
+
"Dimensions have changed: #{change_msg}\n#{base_image_path.to_path}\n#{image_path.to_path}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def annotate_and_save_images
|
46
|
+
save_annotation_for(new_image, annotated_image_path)
|
47
|
+
save_annotation_for(base_image, annotated_base_image_path)
|
48
|
+
save_heatmap_diff if difference.meta[:diff_mask]
|
49
|
+
end
|
50
|
+
|
51
|
+
def save_annotation_for(image, image_path)
|
52
|
+
image = annotate_difference(image, difference.region)
|
53
|
+
image = annotate_skip_areas(image, difference.skip_area) if difference.skip_area
|
54
|
+
|
55
|
+
save(image, image_path.to_path)
|
56
|
+
end
|
57
|
+
|
58
|
+
DIFF_COLOR = [255, 0, 0, 255].freeze
|
59
|
+
|
60
|
+
def annotate_difference(image, region)
|
61
|
+
driver.draw_rectangles([image], region, DIFF_COLOR, offset: 1).first
|
62
|
+
end
|
63
|
+
|
64
|
+
SKIP_COLOR = [255, 192, 0, 255].freeze
|
65
|
+
|
66
|
+
def annotate_skip_areas(image, skip_areas)
|
67
|
+
skip_areas.reduce(image) do |memo, region|
|
68
|
+
driver.draw_rectangles([memo], region, SKIP_COLOR).first
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def save(image, image_path)
|
73
|
+
driver.save_image_to(image, image_path.to_s)
|
74
|
+
end
|
75
|
+
|
76
|
+
NEW_LINE = "\n"
|
77
|
+
|
78
|
+
def build_error_message
|
79
|
+
[
|
80
|
+
"(#{difference.inspect})",
|
81
|
+
image_path.to_path,
|
82
|
+
annotated_base_image_path.to_path,
|
83
|
+
annotated_image_path.to_path
|
84
|
+
].join(NEW_LINE)
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def save_heatmap_diff
|
90
|
+
merged_image = driver.merge(new_image, base_image)
|
91
|
+
highlighted_mask = driver.highlight_mask(difference.meta[:diff_mask], merged_image, color: DIFF_COLOR)
|
92
|
+
|
93
|
+
save(highlighted_mask, heatmap_diff_path.to_path)
|
94
|
+
end
|
95
|
+
|
96
|
+
def base_image
|
97
|
+
difference.comparison.base_image
|
98
|
+
end
|
99
|
+
|
100
|
+
def new_image
|
101
|
+
difference.comparison.new_image
|
102
|
+
end
|
103
|
+
|
104
|
+
def base_image_path
|
105
|
+
comparison.base_image_path
|
106
|
+
end
|
107
|
+
|
108
|
+
def image_path
|
109
|
+
comparison.new_image_path
|
110
|
+
end
|
111
|
+
|
112
|
+
def driver
|
113
|
+
@_driver ||= comparison.driver
|
114
|
+
end
|
115
|
+
|
116
|
+
def comparison
|
117
|
+
@_comparison ||= difference.comparison
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
@@ -1,94 +1,80 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require "capybara_screenshot_diff/snap_manager"
|
3
4
|
require_relative "screenshoter"
|
4
5
|
require_relative "stable_screenshoter"
|
5
6
|
require_relative "browser_helpers"
|
6
7
|
require_relative "vcs"
|
8
|
+
require_relative "area_calculator"
|
7
9
|
|
8
10
|
module Capybara
|
9
11
|
module Screenshot
|
10
12
|
module Diff
|
11
13
|
class ScreenshotMatcher
|
12
|
-
attr_reader :screenshot_full_name, :driver_options, :
|
14
|
+
attr_reader :screenshot_full_name, :driver_options, :screenshot_format
|
13
15
|
|
14
16
|
def initialize(screenshot_full_name, options = {})
|
15
17
|
@screenshot_full_name = screenshot_full_name
|
16
18
|
@driver_options = Diff.default_options.merge(options)
|
17
19
|
|
18
|
-
@
|
19
|
-
@
|
20
|
+
@screenshot_format = @driver_options[:screenshot_format]
|
21
|
+
@snapshot = CapybaraScreenshotDiff::SnapManager.snapshot(screenshot_full_name, @screenshot_format)
|
20
22
|
end
|
21
23
|
|
22
24
|
def build_screenshot_matches_job
|
23
25
|
# TODO: Move this into screenshot stage, in order to re-evaluate coordinates after page updates
|
24
26
|
return if BrowserHelpers.window_size_is_wrong?(Screenshot.window_size)
|
25
27
|
|
26
|
-
# Stability Screenshoter Options
|
27
|
-
|
28
28
|
# TODO: Move this into screenshot stage, in order to re-evaluate coordinates after page updates
|
29
|
-
|
29
|
+
area_calculator = AreaCalculator.new(driver_options.delete(:crop), driver_options[:skip_area])
|
30
|
+
driver_options[:crop] = area_calculator.calculate_crop
|
30
31
|
|
31
|
-
# Allow nil or single or multiple areas
|
32
32
|
# TODO: Move this into screenshot stage, in order to re-evaluate coordinates after page updates
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
37
|
-
driver_options[:driver] = Drivers.for(driver_options)
|
33
|
+
# Allow nil or single or multiple areas
|
34
|
+
driver_options[:skip_area] = area_calculator.calculate_skip_area
|
35
|
+
driver_options[:driver] = Drivers.for(driver_options[:driver])
|
38
36
|
|
39
|
-
|
37
|
+
@snapshot.checkout_base_screenshot
|
40
38
|
|
41
|
-
|
39
|
+
# When fail_if_new is true no need to create screenshot if base screenshot is missing
|
40
|
+
return if Capybara::Screenshot::Diff.fail_if_new && !@snapshot.base_path.exist?
|
42
41
|
|
43
|
-
capture_options =
|
44
|
-
crop: crop,
|
45
|
-
stability_time_limit: driver_options.delete(:stability_time_limit),
|
46
|
-
wait: driver_options.delete(:wait)
|
47
|
-
}
|
42
|
+
capture_options, comparison_options = extract_capture_and_comparison_options!(driver_options)
|
48
43
|
|
49
|
-
|
44
|
+
# Load new screenshot from Browser
|
45
|
+
take_comparison_screenshot(capture_options, comparison_options, @snapshot)
|
50
46
|
|
51
|
-
|
47
|
+
# Pre-computation: No need to compare without base screenshot
|
48
|
+
return unless @snapshot.base_path.exist?
|
52
49
|
|
53
50
|
# Add comparison job in the queue
|
54
|
-
[
|
55
|
-
screenshot_full_name,
|
56
|
-
ImageCompare.new(screenshot_path.to_s, base_screenshot_path.to_s, driver_options)
|
57
|
-
]
|
58
|
-
end
|
59
|
-
|
60
|
-
def cleanup
|
61
|
-
FileUtils.rm_f(base_screenshot_path)
|
62
|
-
end
|
63
|
-
|
64
|
-
def self.base_image_path_from(screenshot_path)
|
65
|
-
screenshot_path.sub_ext(".base.png")
|
51
|
+
[screenshot_full_name, ImageCompare.new(@snapshot.path, @snapshot.base_path, comparison_options)]
|
66
52
|
end
|
67
53
|
|
68
54
|
private
|
69
55
|
|
70
|
-
def
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
56
|
+
def extract_capture_and_comparison_options!(driver_options = {})
|
57
|
+
[
|
58
|
+
{
|
59
|
+
# screenshot options
|
60
|
+
capybara_screenshot_options: driver_options[:capybara_screenshot_options],
|
61
|
+
crop: driver_options.delete(:crop),
|
62
|
+
# delivery options
|
63
|
+
screenshot_format: driver_options[:screenshot_format],
|
64
|
+
# stability options
|
65
|
+
stability_time_limit: driver_options.delete(:stability_time_limit),
|
66
|
+
wait: driver_options.delete(:wait)
|
67
|
+
},
|
68
|
+
driver_options
|
69
|
+
]
|
84
70
|
end
|
85
71
|
|
86
72
|
# Try to get screenshot from browser.
|
87
73
|
# On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts
|
88
74
|
# On reaching `wait` limit then it has been failed. On failing we annotate screenshot attempts to help to debug
|
89
|
-
def take_comparison_screenshot(capture_options,
|
90
|
-
screenshoter = build_screenshoter_for(capture_options,
|
91
|
-
screenshoter.take_comparison_screenshot(
|
75
|
+
def take_comparison_screenshot(capture_options, comparison_options, snapshot = nil)
|
76
|
+
screenshoter = build_screenshoter_for(capture_options, comparison_options)
|
77
|
+
screenshoter.take_comparison_screenshot(snapshot)
|
92
78
|
end
|
93
79
|
|
94
80
|
def build_screenshoter_for(capture_options, comparison_options = {})
|
@@ -98,30 +84,6 @@ module Capybara
|
|
98
84
|
Diff.screenshoter.new(capture_options, comparison_options[:driver])
|
99
85
|
end
|
100
86
|
end
|
101
|
-
|
102
|
-
# Cast skip areas params into Region
|
103
|
-
# and if there is crop then makes absolute coordinates to eb relative to crop top left corner
|
104
|
-
def calculate_skip_area(skip_area, crop)
|
105
|
-
crop_region = crop && Region.new(*crop)
|
106
|
-
skip_area = Array(skip_area)
|
107
|
-
|
108
|
-
css_selectors, regions = skip_area.compact.partition { |region| region.is_a? String }
|
109
|
-
|
110
|
-
result = []
|
111
|
-
unless css_selectors.empty?
|
112
|
-
result.concat(build_regions_for(BrowserHelpers.bounds_for_css(*css_selectors)))
|
113
|
-
end
|
114
|
-
result.concat(build_regions_for(regions.flatten.each_slice(4))) unless regions.empty?
|
115
|
-
result.compact!
|
116
|
-
|
117
|
-
result.map! { |region| crop_region.find_relative_intersect(region) } if crop_region
|
118
|
-
|
119
|
-
result
|
120
|
-
end
|
121
|
-
|
122
|
-
def build_regions_for(coordinates)
|
123
|
-
coordinates.map { |coordinates_entity| Region.from_edge_coordinates(*coordinates_entity) }
|
124
|
-
end
|
125
87
|
end
|
126
88
|
end
|
127
89
|
end
|
@@ -6,11 +6,10 @@ require_relative "browser_helpers"
|
|
6
6
|
module Capybara
|
7
7
|
module Screenshot
|
8
8
|
class Screenshoter
|
9
|
-
attr_reader :capture_options, :
|
9
|
+
attr_reader :capture_options, :driver
|
10
10
|
|
11
11
|
def initialize(capture_options, driver)
|
12
12
|
@capture_options = capture_options
|
13
|
-
@comparison_options = comparison_options
|
14
13
|
@driver = driver
|
15
14
|
end
|
16
15
|
|
@@ -22,88 +21,54 @@ module Capybara
|
|
22
21
|
@capture_options[:wait]
|
23
22
|
end
|
24
23
|
|
25
|
-
def
|
26
|
-
|
27
|
-
end
|
28
|
-
|
29
|
-
def self.cleanup_attempts_screenshots(base_file)
|
30
|
-
FileUtils.rm_rf attempts_screenshot_paths(base_file)
|
24
|
+
def capybara_screenshot_options
|
25
|
+
@capture_options[:capybara_screenshot_options] || {}
|
31
26
|
end
|
32
27
|
|
33
28
|
# Try to get screenshot from browser.
|
34
29
|
# On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts
|
35
30
|
# On reaching `wait` limit then it has been failed. On failing we annotate screenshot attempts to help to debug
|
36
|
-
def take_comparison_screenshot(
|
37
|
-
|
38
|
-
|
39
|
-
take_screenshot(new_screenshot_path)
|
40
|
-
|
41
|
-
FileUtils.mv(new_screenshot_path, screenshot_path, force: true)
|
42
|
-
Screenshoter.cleanup_attempts_screenshots(screenshot_path)
|
31
|
+
def take_comparison_screenshot(snapshot)
|
32
|
+
capture_screenshot_at(snapshot)
|
33
|
+
snapshot.cleanup_attempts
|
43
34
|
end
|
44
35
|
|
45
|
-
|
46
|
-
Pathname.new(screenshot_path).sub_ext(format(".attempt_%02i.png", iteration))
|
47
|
-
end
|
36
|
+
PNG_EXTENSION = ".png"
|
48
37
|
|
49
38
|
def take_screenshot(screenshot_path)
|
50
39
|
blurred_input = prepare_page_for_screenshot(timeout: wait)
|
51
40
|
|
52
41
|
# Take browser screenshot and save
|
53
|
-
|
42
|
+
save_and_process_screenshot(screenshot_path)
|
54
43
|
|
55
|
-
# Load saved screenshot and pre-process it
|
56
|
-
process_screenshot(screenshot_path)
|
57
|
-
ensure
|
58
44
|
blurred_input&.click
|
59
45
|
end
|
60
46
|
|
61
|
-
def
|
62
|
-
|
63
|
-
end
|
47
|
+
def process_screenshot(stored_path, screenshot_path)
|
48
|
+
screenshot_image = driver.from_file(stored_path)
|
64
49
|
|
65
|
-
def process_screenshot(screenshot_path)
|
66
50
|
# TODO(uwe): Remove when chromedriver takes right size screenshots
|
67
51
|
# TODO: Adds tests when this case is true
|
68
|
-
if selenium_with_retina_screen?
|
69
|
-
reduce_retina_image_size(screenshot_path)
|
70
|
-
end
|
52
|
+
screenshot_image = resize_if_needed(screenshot_image) if selenium_with_retina_screen?
|
71
53
|
# ODOT
|
72
54
|
|
73
|
-
if crop
|
74
|
-
image = driver.from_file(screenshot_path)
|
75
|
-
cropped_image = driver.crop(crop, image)
|
76
|
-
driver.save_image_to(cropped_image, screenshot_path)
|
77
|
-
end
|
78
|
-
end
|
79
|
-
|
80
|
-
def reduce_retina_image_size(file_name)
|
81
|
-
expected_image_width = Screenshot.window_size[0]
|
82
|
-
saved_image = driver.from_file(file_name.to_s)
|
83
|
-
return if driver.width_for(saved_image) < expected_image_width * 2
|
84
|
-
|
85
|
-
notice_how_to_avoid_this
|
86
|
-
|
87
|
-
new_height = expected_image_width * driver.height_for(saved_image) / driver.width_for(saved_image)
|
88
|
-
resized_image = driver.resize_image_to(saved_image, expected_image_width, new_height)
|
55
|
+
screenshot_image = driver.crop(crop, screenshot_image) if crop
|
89
56
|
|
90
|
-
driver.save_image_to(
|
57
|
+
driver.save_image_to(screenshot_image, screenshot_path)
|
91
58
|
end
|
92
59
|
|
93
60
|
def notice_how_to_avoid_this
|
94
61
|
unless defined?(@_csd_retina_warned)
|
95
62
|
warn "Halving retina screenshot. " \
|
96
|
-
|
63
|
+
'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
|
97
64
|
@_csd_retina_warned = true
|
98
65
|
end
|
99
66
|
end
|
100
67
|
|
101
68
|
def prepare_page_for_screenshot(timeout:)
|
102
|
-
wait_images_loaded(timeout: timeout)
|
69
|
+
wait_images_loaded(timeout: timeout) if timeout
|
103
70
|
|
104
|
-
blurred_input = if Screenshot.blur_active_element
|
105
|
-
BrowserHelpers.blur_from_focused_element
|
106
|
-
end
|
71
|
+
blurred_input = BrowserHelpers.blur_from_focused_element if Screenshot.blur_active_element
|
107
72
|
|
108
73
|
if Screenshot.hide_caret
|
109
74
|
BrowserHelpers.hide_caret
|
@@ -113,15 +78,16 @@ module Capybara
|
|
113
78
|
end
|
114
79
|
|
115
80
|
def wait_images_loaded(timeout:)
|
116
|
-
|
81
|
+
return unless timeout
|
82
|
+
|
83
|
+
deadline_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
117
84
|
loop do
|
118
85
|
pending_image = BrowserHelpers.pending_image_to_load
|
119
86
|
break unless pending_image
|
120
87
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
)
|
88
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline_at
|
89
|
+
raise CapybaraScreenshotDiff::ExpectationNotMet, "Images have not been loaded after #{timeout}s: #{pending_image.inspect}"
|
90
|
+
end
|
125
91
|
|
126
92
|
sleep 0.025
|
127
93
|
end
|
@@ -129,6 +95,31 @@ module Capybara
|
|
129
95
|
|
130
96
|
private
|
131
97
|
|
98
|
+
def save_and_process_screenshot(screenshot_path)
|
99
|
+
tmpfile = Tempfile.new([screenshot_path.basename.to_s, PNG_EXTENSION])
|
100
|
+
BrowserHelpers.session.save_screenshot(tmpfile.path, **capybara_screenshot_options)
|
101
|
+
# Load saved screenshot and pre-process it
|
102
|
+
process_screenshot(tmpfile.path, screenshot_path)
|
103
|
+
ensure
|
104
|
+
File.unlink(tmpfile) if tmpfile
|
105
|
+
end
|
106
|
+
|
107
|
+
def capture_screenshot_at(snapshot)
|
108
|
+
take_screenshot(snapshot.next_attempt_path!)
|
109
|
+
|
110
|
+
snapshot.commit_last_attempt
|
111
|
+
end
|
112
|
+
|
113
|
+
def resize_if_needed(saved_image)
|
114
|
+
expected_image_width = Screenshot.window_size[0]
|
115
|
+
return saved_image if driver.width_for(saved_image) < expected_image_width * 2
|
116
|
+
|
117
|
+
notice_how_to_avoid_this
|
118
|
+
|
119
|
+
new_height = expected_image_width * driver.height_for(saved_image) / driver.width_for(saved_image)
|
120
|
+
driver.resize_image_to(saved_image, expected_image_width, new_height)
|
121
|
+
end
|
122
|
+
|
132
123
|
def selenium_with_retina_screen?
|
133
124
|
Os::ON_MAC && BrowserHelpers.selenium? && Screenshot.window_size
|
134
125
|
end
|
@@ -8,97 +8,99 @@ module Capybara
|
|
8
8
|
|
9
9
|
attr_reader :stability_time_limit, :wait
|
10
10
|
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
11
|
+
# Initializes a new instance of StableScreenshoter
|
12
|
+
#
|
13
|
+
# This method sets up a new screenshoter with specific capture and comparison options. It validates the presence of
|
14
|
+
# `:stability_time_limit` and `:wait` in capture options and ensures that `:stability_time_limit` is less than or equal to `:wait`.
|
15
|
+
#
|
16
|
+
# @param capture_options [Hash] The options for capturing screenshots, must include `:stability_time_limit` and `:wait`.
|
17
|
+
# @param comparison_options [Hash, nil] The options for comparing screenshots, defaults to `nil` which uses `Diff.default_options`.
|
18
|
+
# @raise [ArgumentError] If `:wait` or `:stability_time_limit` are not provided, or if `:stability_time_limit` is greater than `:wait`.
|
19
|
+
def initialize(capture_options, comparison_options = {})
|
20
|
+
@stability_time_limit, @wait = capture_options.fetch_values(*STABILITY_OPTIONS)
|
21
|
+
|
22
|
+
raise ArgumentError, "wait should be provided for stable screenshots" unless wait
|
23
|
+
raise ArgumentError, "stability_time_limit should be provided for stable screenshots" unless stability_time_limit
|
24
|
+
raise ArgumentError, "stability_time_limit (#{stability_time_limit}) should be less or equal than wait (#{wait}) for stable screenshots" unless stability_time_limit <= wait
|
25
|
+
|
26
|
+
@comparison_options = comparison_options
|
27
|
+
|
28
|
+
driver = Diff::Drivers.for(@comparison_options)
|
29
|
+
@screenshoter = Diff.screenshoter.new(capture_options.except(:stability_time_limit), driver)
|
15
30
|
end
|
16
31
|
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
20
|
-
|
21
|
-
|
32
|
+
# Takes a comparison screenshot ensuring page stability
|
33
|
+
#
|
34
|
+
# Attempts to take a stable screenshot of the page by comparing several screenshot attempts until the page stops updating
|
35
|
+
# or the `:wait` limit is reached. If unable to achieve a stable state within the time limit, it annotates the attempts
|
36
|
+
# to aid debugging.
|
37
|
+
#
|
38
|
+
# @param snapshot Snap The snapshot details to take a stable screenshot of.
|
39
|
+
# @return [void]
|
40
|
+
# @raise [RuntimeError] If a stable screenshot cannot be obtained within the specified `:wait` time.
|
41
|
+
def take_comparison_screenshot(snapshot)
|
42
|
+
result = take_stable_screenshot(snapshot)
|
22
43
|
|
23
44
|
# We failed to get stable browser state! Generate difference between attempts to overview moving parts!
|
24
|
-
unless
|
45
|
+
unless result
|
25
46
|
# FIXME(uwe): Change to store the failure and only report if the test succeeds functionally.
|
26
|
-
annotate_attempts_and_fail!(
|
47
|
+
annotate_attempts_and_fail!(snapshot)
|
27
48
|
end
|
28
49
|
|
29
|
-
|
30
|
-
|
50
|
+
# store success attempt as actual screenshot
|
51
|
+
snapshot.commit_last_attempt
|
52
|
+
|
53
|
+
# cleanup all previous attempts
|
54
|
+
snapshot.cleanup_attempts
|
31
55
|
end
|
32
56
|
|
33
|
-
def take_stable_screenshot(
|
57
|
+
def take_stable_screenshot(snapshot)
|
34
58
|
# We try to compare first attempt with checkout version, in order to not run next screenshots
|
35
|
-
|
36
|
-
screenshot_started_at = last_attempt_at = Time.now
|
59
|
+
deadline_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + wait
|
37
60
|
|
38
61
|
# Cleanup all previous attempts for sure
|
39
|
-
|
62
|
+
snapshot.cleanup_attempts
|
40
63
|
|
41
64
|
0.step do |i|
|
42
|
-
#
|
43
|
-
sleep(stability_time_limit) unless i == 0
|
44
|
-
|
45
|
-
elapsed_time = last_attempt_at - screenshot_started_at
|
46
|
-
|
47
|
-
prev_attempt_path = attempt_path
|
48
|
-
attempt_path = Screenshoter.gen_next_attempt_path(screenshot_path, i)
|
49
|
-
|
50
|
-
@screenshoter.take_screenshot(attempt_path)
|
51
|
-
last_attempt_at = Time.now
|
65
|
+
# FIXME: it should be wait, and wait should be replaced with stability_time_limit
|
66
|
+
sleep(stability_time_limit) unless i == 0 # test prev_attempt_path is nil
|
52
67
|
|
53
|
-
|
54
|
-
stabilization_comparator = build_comparison_for(attempt_path, prev_attempt_path)
|
68
|
+
attempt_next_screenshot(snapshot)
|
55
69
|
|
56
|
-
|
57
|
-
return
|
58
|
-
|
59
|
-
# If timeout then we failed to generate valid screenshot
|
60
|
-
return nil if timeout?(elapsed_time)
|
70
|
+
return true if attempt_successful?(snapshot)
|
71
|
+
return false if timeout?(deadline_at)
|
61
72
|
end
|
62
73
|
end
|
63
74
|
|
64
75
|
private
|
65
76
|
|
66
|
-
def
|
67
|
-
|
68
|
-
end
|
69
|
-
|
70
|
-
def annotate_attempts_and_fail!(screenshot_path)
|
71
|
-
screenshot_attempts = Screenshoter.attempts_screenshot_paths(screenshot_path)
|
72
|
-
|
73
|
-
annotate_stabilization_images(screenshot_attempts)
|
77
|
+
def attempt_successful?(snapshot)
|
78
|
+
return false unless snapshot.prev_attempt_path
|
74
79
|
|
75
|
-
|
76
|
-
|
80
|
+
build_last_attempts_comparison_for(snapshot).quick_equal?
|
81
|
+
rescue ArgumentError
|
82
|
+
false
|
77
83
|
end
|
78
84
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
attempts_screenshot_paths.reverse_each do |file_name|
|
83
|
-
if previous_file && File.exist?(previous_file)
|
84
|
-
attempts_comparison = build_comparison_for(file_name, previous_file)
|
85
|
-
|
86
|
-
if attempts_comparison.different?
|
87
|
-
FileUtils.mv(attempts_comparison.annotated_base_image_path, previous_file, force: true)
|
88
|
-
else
|
89
|
-
warn "[capybara-screenshot-diff] Some attempts was stable, but mistakenly marked as not: " \
|
90
|
-
"#{previous_file} and #{file_name} are equal"
|
91
|
-
end
|
85
|
+
def attempt_next_screenshot(snapshot)
|
86
|
+
@screenshoter.take_screenshot(snapshot.next_attempt_path!)
|
87
|
+
end
|
92
88
|
|
93
|
-
|
94
|
-
|
89
|
+
def timeout?(deadline_at)
|
90
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline_at
|
91
|
+
end
|
95
92
|
|
96
|
-
|
97
|
-
|
93
|
+
def build_last_attempts_comparison_for(snapshot)
|
94
|
+
ImageCompare.new(snapshot.attempt_path, snapshot.prev_attempt_path, @comparison_options)
|
98
95
|
end
|
99
96
|
|
100
|
-
|
101
|
-
|
97
|
+
# TODO: Move to the HistoricalReporter
|
98
|
+
def annotate_attempts_and_fail!(snapshot)
|
99
|
+
require "capybara_screenshot_diff/attempts_reporter"
|
100
|
+
attempts_reporter = CapybaraScreenshotDiff::AttemptsReporter.new(snapshot, @comparison_options, {wait: wait, stability_time_limit: stability_time_limit})
|
101
|
+
|
102
|
+
# TODO: Move fail to the queue after tests passed
|
103
|
+
fail(attempts_reporter.generate)
|
102
104
|
end
|
103
105
|
end
|
104
106
|
end
|