capybara-screenshot-diff 1.8.3 → 1.9.0
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 +1 -2
- data/gems.rb +4 -4
- data/lib/capybara/screenshot/diff/area_calculator.rb +56 -0
- data/lib/capybara/screenshot/diff/browser_helpers.rb +1 -1
- 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 -4
- data/lib/capybara/screenshot/diff/drivers/chunky_png_driver.rb +10 -5
- data/lib/capybara/screenshot/diff/drivers/vips_driver.rb +14 -3
- data/lib/capybara/screenshot/diff/drivers.rb +1 -1
- data/lib/capybara/screenshot/diff/image_compare.rb +80 -114
- data/lib/capybara/screenshot/diff/region.rb +28 -7
- data/lib/capybara/screenshot/diff/reporters/default.rb +117 -0
- data/lib/capybara/screenshot/diff/screenshot_matcher.rb +24 -53
- data/lib/capybara/screenshot/diff/screenshoter.rb +61 -42
- data/lib/capybara/screenshot/diff/stable_screenshoter.rb +51 -29
- data/lib/capybara/screenshot/diff/test_methods.rb +75 -8
- data/lib/capybara/screenshot/diff/{drivers/utils.rb → utils.rb} +0 -7
- data/lib/capybara/screenshot/diff/vcs.rb +1 -1
- 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/cucumber.rb +12 -0
- data/lib/capybara_screenshot_diff/dsl.rb +10 -0
- data/lib/capybara_screenshot_diff/minitest.rb +45 -0
- data/lib/capybara_screenshot_diff/rspec.rb +31 -0
- data/lib/capybara_screenshot_diff.rb +85 -0
- metadata +15 -37
- 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
@@ -0,0 +1,117 @@
|
|
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
|
+
"Screenshot dimension has been changed for #{image_path.to_path}: #{change_msg}"
|
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 image_path
|
105
|
+
comparison.new_image_path
|
106
|
+
end
|
107
|
+
|
108
|
+
def driver
|
109
|
+
@_driver ||= comparison.driver
|
110
|
+
end
|
111
|
+
|
112
|
+
def comparison
|
113
|
+
@_comparison ||= difference.comparison
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
@@ -4,18 +4,20 @@ require_relative "screenshoter"
|
|
4
4
|
require_relative "stable_screenshoter"
|
5
5
|
require_relative "browser_helpers"
|
6
6
|
require_relative "vcs"
|
7
|
+
require_relative "area_calculator"
|
7
8
|
|
8
9
|
module Capybara
|
9
10
|
module Screenshot
|
10
11
|
module Diff
|
11
12
|
class ScreenshotMatcher
|
12
|
-
attr_reader :screenshot_full_name, :driver_options, :screenshot_path, :base_screenshot_path
|
13
|
+
attr_reader :screenshot_full_name, :driver_options, :screenshot_path, :base_screenshot_path, :screenshot_format
|
13
14
|
|
14
15
|
def initialize(screenshot_full_name, options = {})
|
15
16
|
@screenshot_full_name = screenshot_full_name
|
16
17
|
@driver_options = Diff.default_options.merge(options)
|
17
18
|
|
18
|
-
@
|
19
|
+
@screenshot_format = @driver_options[:screenshot_format]
|
20
|
+
@screenshot_path = Screenshot.screenshot_area_abs / Pathname.new(screenshot_full_name).sub_ext(".#{screenshot_format}")
|
19
21
|
@base_screenshot_path = ScreenshotMatcher.base_image_path_from(@screenshot_path)
|
20
22
|
end
|
21
23
|
|
@@ -23,46 +25,47 @@ module Capybara
|
|
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
|
-
|
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
|
+
|
36
|
+
driver_options[:driver] = Drivers.for(driver_options[:driver])
|
38
37
|
|
38
|
+
# Load base screenshot from VCS
|
39
39
|
create_output_directory_for(screenshot_path) unless screenshot_path.exist?
|
40
40
|
|
41
41
|
checkout_base_screenshot
|
42
42
|
|
43
|
+
# When fail_if_new is true no need to create screenshot if base screenshot is missing
|
44
|
+
return if Capybara::Screenshot::Diff.fail_if_new && !base_screenshot_path.exist?
|
45
|
+
|
43
46
|
capture_options = {
|
44
|
-
|
47
|
+
# screenshot options
|
48
|
+
capybara_screenshot_options: driver_options[:capybara_screenshot_options],
|
49
|
+
crop: driver_options.delete(:crop),
|
50
|
+
# delivery options
|
51
|
+
screenshot_format: driver_options[:screenshot_format],
|
52
|
+
# stability options
|
45
53
|
stability_time_limit: driver_options.delete(:stability_time_limit),
|
46
54
|
wait: driver_options.delete(:wait)
|
47
55
|
}
|
48
56
|
|
57
|
+
# Load new screenshot from Browser
|
49
58
|
take_comparison_screenshot(capture_options, driver_options, screenshot_path)
|
50
59
|
|
60
|
+
# Pre-computation: No need to compare without base screenshot
|
51
61
|
return unless base_screenshot_path.exist?
|
52
62
|
|
53
63
|
# 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)
|
64
|
+
[screenshot_full_name, ImageCompare.new(screenshot_path, base_screenshot_path, driver_options)]
|
62
65
|
end
|
63
66
|
|
64
67
|
def self.base_image_path_from(screenshot_path)
|
65
|
-
screenshot_path.sub_ext(".base.
|
68
|
+
screenshot_path.sub_ext(".base#{screenshot_path.extname}")
|
66
69
|
end
|
67
70
|
|
68
71
|
private
|
@@ -71,14 +74,6 @@ module Capybara
|
|
71
74
|
Vcs.checkout_vcs(screenshot_path, base_screenshot_path)
|
72
75
|
end
|
73
76
|
|
74
|
-
def calculate_crop_region(driver_options)
|
75
|
-
crop_coordinates = driver_options.delete(:crop)
|
76
|
-
return nil unless crop_coordinates
|
77
|
-
|
78
|
-
crop_coordinates = BrowserHelpers.bounds_for_css(crop_coordinates).first if crop_coordinates.is_a?(String)
|
79
|
-
Region.from_edge_coordinates(*crop_coordinates)
|
80
|
-
end
|
81
|
-
|
82
77
|
def create_output_directory_for(screenshot_path)
|
83
78
|
screenshot_path.dirname.mkpath
|
84
79
|
end
|
@@ -98,30 +93,6 @@ module Capybara
|
|
98
93
|
Diff.screenshoter.new(capture_options, comparison_options[:driver])
|
99
94
|
end
|
100
95
|
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
96
|
end
|
126
97
|
end
|
127
98
|
end
|
@@ -22,8 +22,17 @@ module Capybara
|
|
22
22
|
@capture_options[:wait]
|
23
23
|
end
|
24
24
|
|
25
|
+
def screenshot_format
|
26
|
+
@capture_options[:screenshot_format] || "png"
|
27
|
+
end
|
28
|
+
|
29
|
+
def capybara_screenshot_options
|
30
|
+
@capture_options[:capybara_screenshot_options] || {}
|
31
|
+
end
|
32
|
+
|
25
33
|
def self.attempts_screenshot_paths(base_file)
|
26
|
-
|
34
|
+
extname = Pathname.new(base_file).extname
|
35
|
+
Dir["#{base_file.to_s.chomp(extname)}.attempt_*#{extname}"].sort
|
27
36
|
end
|
28
37
|
|
29
38
|
def self.cleanup_attempts_screenshots(base_file)
|
@@ -34,76 +43,51 @@ module Capybara
|
|
34
43
|
# On `stability_time_limit` it checks that page stop updating by comparison several screenshot attempts
|
35
44
|
# On reaching `wait` limit then it has been failed. On failing we annotate screenshot attempts to help to debug
|
36
45
|
def take_comparison_screenshot(screenshot_path)
|
37
|
-
|
46
|
+
capture_screenshot_at(screenshot_path)
|
38
47
|
|
39
|
-
take_screenshot(new_screenshot_path)
|
40
|
-
|
41
|
-
FileUtils.mv(new_screenshot_path, screenshot_path, force: true)
|
42
48
|
Screenshoter.cleanup_attempts_screenshots(screenshot_path)
|
43
49
|
end
|
44
50
|
|
45
51
|
def self.gen_next_attempt_path(screenshot_path, iteration)
|
46
|
-
|
52
|
+
screenshot_path.sub_ext(format(".attempt_%02i#{screenshot_path.extname}", iteration))
|
47
53
|
end
|
48
54
|
|
55
|
+
PNG_EXTENSION = ".png"
|
56
|
+
|
49
57
|
def take_screenshot(screenshot_path)
|
50
58
|
blurred_input = prepare_page_for_screenshot(timeout: wait)
|
51
59
|
|
52
60
|
# Take browser screenshot and save
|
53
|
-
|
61
|
+
save_and_process_screenshot(screenshot_path)
|
54
62
|
|
55
|
-
# Load saved screenshot and pre-process it
|
56
|
-
process_screenshot(screenshot_path)
|
57
|
-
ensure
|
58
63
|
blurred_input&.click
|
59
64
|
end
|
60
65
|
|
61
|
-
def
|
62
|
-
|
63
|
-
end
|
66
|
+
def process_screenshot(stored_path, screenshot_path)
|
67
|
+
screenshot_image = driver.from_file(stored_path)
|
64
68
|
|
65
|
-
def process_screenshot(screenshot_path)
|
66
69
|
# TODO(uwe): Remove when chromedriver takes right size screenshots
|
67
70
|
# TODO: Adds tests when this case is true
|
68
|
-
if selenium_with_retina_screen?
|
69
|
-
reduce_retina_image_size(screenshot_path)
|
70
|
-
end
|
71
|
+
screenshot_image = resize_if_needed(screenshot_image) if selenium_with_retina_screen?
|
71
72
|
# ODOT
|
72
73
|
|
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
|
74
|
+
screenshot_image = driver.crop(crop, screenshot_image) if crop
|
84
75
|
|
85
|
-
|
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)
|
89
|
-
|
90
|
-
driver.save_image_to(resized_image, file_name)
|
76
|
+
driver.save_image_to(screenshot_image, screenshot_path)
|
91
77
|
end
|
92
78
|
|
93
79
|
def notice_how_to_avoid_this
|
94
80
|
unless defined?(@_csd_retina_warned)
|
95
81
|
warn "Halving retina screenshot. " \
|
96
|
-
|
82
|
+
'You should add "force-device-scale-factor=1" to your Chrome chromeOptions args.'
|
97
83
|
@_csd_retina_warned = true
|
98
84
|
end
|
99
85
|
end
|
100
86
|
|
101
87
|
def prepare_page_for_screenshot(timeout:)
|
102
|
-
wait_images_loaded(timeout: timeout)
|
88
|
+
wait_images_loaded(timeout: timeout) if timeout
|
103
89
|
|
104
|
-
blurred_input = if Screenshot.blur_active_element
|
105
|
-
BrowserHelpers.blur_from_focused_element
|
106
|
-
end
|
90
|
+
blurred_input = BrowserHelpers.blur_from_focused_element if Screenshot.blur_active_element
|
107
91
|
|
108
92
|
if Screenshot.hide_caret
|
109
93
|
BrowserHelpers.hide_caret
|
@@ -113,13 +97,15 @@ module Capybara
|
|
113
97
|
end
|
114
98
|
|
115
99
|
def wait_images_loaded(timeout:)
|
116
|
-
|
100
|
+
return unless timeout
|
101
|
+
|
102
|
+
deadline_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
117
103
|
loop do
|
118
104
|
pending_image = BrowserHelpers.pending_image_to_load
|
119
105
|
break unless pending_image
|
120
106
|
|
121
|
-
if (
|
122
|
-
raise
|
107
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline_at
|
108
|
+
raise CapybaraScreenshotDiff::ExpectationNotMet, "Images have not been loaded after #{timeout}s: #{pending_image.inspect}"
|
123
109
|
end
|
124
110
|
|
125
111
|
sleep 0.025
|
@@ -128,6 +114,39 @@ module Capybara
|
|
128
114
|
|
129
115
|
private
|
130
116
|
|
117
|
+
def save_and_process_screenshot(screenshot_path)
|
118
|
+
tmpfile = Tempfile.new([screenshot_path.basename.to_s, PNG_EXTENSION])
|
119
|
+
BrowserHelpers.session.save_screenshot(tmpfile.path, **capybara_screenshot_options)
|
120
|
+
# Load saved screenshot and pre-process it
|
121
|
+
process_screenshot(tmpfile.path, screenshot_path)
|
122
|
+
ensure
|
123
|
+
File.unlink(tmpfile) if tmpfile
|
124
|
+
end
|
125
|
+
|
126
|
+
def capture_screenshot_at(screenshot_path)
|
127
|
+
new_screenshot_path = Screenshoter.gen_next_attempt_path(screenshot_path, 0)
|
128
|
+
take_and_process_screenshot(new_screenshot_path, screenshot_path)
|
129
|
+
end
|
130
|
+
|
131
|
+
def take_and_process_screenshot(new_screenshot_path, screenshot_path)
|
132
|
+
take_screenshot(new_screenshot_path)
|
133
|
+
move_screenshot_to(new_screenshot_path, screenshot_path)
|
134
|
+
end
|
135
|
+
|
136
|
+
def move_screenshot_to(new_screenshot_path, screenshot_path)
|
137
|
+
FileUtils.mv(new_screenshot_path, screenshot_path, force: true)
|
138
|
+
end
|
139
|
+
|
140
|
+
def resize_if_needed(saved_image)
|
141
|
+
expected_image_width = Screenshot.window_size[0]
|
142
|
+
return saved_image if driver.width_for(saved_image) < expected_image_width * 2
|
143
|
+
|
144
|
+
notice_how_to_avoid_this
|
145
|
+
|
146
|
+
new_height = expected_image_width * driver.height_for(saved_image) / driver.width_for(saved_image)
|
147
|
+
driver.resize_image_to(saved_image, expected_image_width, new_height)
|
148
|
+
end
|
149
|
+
|
131
150
|
def selenium_with_retina_screen?
|
132
151
|
Os::ON_MAC && BrowserHelpers.selenium? && Screenshot.window_size
|
133
152
|
end
|
@@ -8,15 +8,36 @@ module Capybara
|
|
8
8
|
|
9
9
|
attr_reader :stability_time_limit, :wait
|
10
10
|
|
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`.
|
11
19
|
def initialize(capture_options, comparison_options = nil)
|
12
20
|
@stability_time_limit, @wait = capture_options.fetch_values(:stability_time_limit, :wait)
|
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
|
+
|
13
26
|
@comparison_options = comparison_options || Diff.default_options
|
14
|
-
|
27
|
+
|
28
|
+
driver = Diff::Drivers.for(@comparison_options)
|
29
|
+
@screenshoter = Diff.screenshoter.new(capture_options.except(*STABILITY_OPTIONS), driver)
|
15
30
|
end
|
16
31
|
|
17
|
-
#
|
18
|
-
#
|
19
|
-
#
|
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 screenshot_path [String, Pathname] The path where the screenshot will be saved.
|
39
|
+
# @return [void]
|
40
|
+
# @raise [RuntimeError] If a stable screenshot cannot be obtained within the specified `:wait` time.
|
20
41
|
def take_comparison_screenshot(screenshot_path)
|
21
42
|
new_screenshot_path = take_stable_screenshot(screenshot_path)
|
22
43
|
|
@@ -31,42 +52,47 @@ module Capybara
|
|
31
52
|
end
|
32
53
|
|
33
54
|
def take_stable_screenshot(screenshot_path)
|
55
|
+
screenshot_path = screenshot_path.is_a?(String) ? Pathname.new(screenshot_path) : screenshot_path
|
34
56
|
# We try to compare first attempt with checkout version, in order to not run next screenshots
|
35
57
|
attempt_path = nil
|
36
|
-
|
58
|
+
deadline_at = Process.clock_gettime(Process::CLOCK_MONOTONIC) + wait
|
37
59
|
|
38
60
|
# Cleanup all previous attempts for sure
|
39
61
|
Screenshoter.cleanup_attempts_screenshots(screenshot_path)
|
40
62
|
|
41
63
|
0.step do |i|
|
42
|
-
#
|
64
|
+
# FIXME: it should be wait, and wait should be replaced with stability_time_limit
|
43
65
|
sleep(stability_time_limit) unless i == 0
|
66
|
+
attempt_path, prev_attempt_path = attempt_next_screenshot(attempt_path, i, screenshot_path)
|
67
|
+
return attempt_path if attempt_successful?(attempt_path, prev_attempt_path)
|
68
|
+
return nil if timeout?(deadline_at)
|
69
|
+
end
|
70
|
+
end
|
44
71
|
|
45
|
-
|
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
|
52
|
-
|
53
|
-
next unless prev_attempt_path
|
54
|
-
stabilization_comparator = build_comparison_for(attempt_path, prev_attempt_path)
|
72
|
+
private
|
55
73
|
|
56
|
-
|
57
|
-
|
74
|
+
def attempt_successful?(attempt_path, prev_attempt_path)
|
75
|
+
return false unless prev_attempt_path
|
76
|
+
build_comparison_for(attempt_path, prev_attempt_path).quick_equal?
|
77
|
+
rescue ArgumentError
|
78
|
+
false
|
79
|
+
end
|
58
80
|
|
59
|
-
|
60
|
-
|
61
|
-
|
81
|
+
def attempt_next_screenshot(prev_attempt_path, i, screenshot_path)
|
82
|
+
new_attempt_path = Screenshoter.gen_next_attempt_path(screenshot_path, i)
|
83
|
+
@screenshoter.take_screenshot(new_attempt_path)
|
84
|
+
[new_attempt_path, prev_attempt_path]
|
62
85
|
end
|
63
86
|
|
64
|
-
|
87
|
+
def timeout?(deadline_at)
|
88
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC) > deadline_at
|
89
|
+
end
|
65
90
|
|
66
91
|
def build_comparison_for(attempt_path, previous_attempt_path)
|
67
92
|
ImageCompare.new(attempt_path, previous_attempt_path, @comparison_options)
|
68
93
|
end
|
69
94
|
|
95
|
+
# TODO: Move to the HistoricalReporter
|
70
96
|
def annotate_attempts_and_fail!(screenshot_path)
|
71
97
|
screenshot_attempts = Screenshoter.attempts_screenshot_paths(screenshot_path)
|
72
98
|
|
@@ -84,22 +110,18 @@ module Capybara
|
|
84
110
|
attempts_comparison = build_comparison_for(file_name, previous_file)
|
85
111
|
|
86
112
|
if attempts_comparison.different?
|
87
|
-
FileUtils.mv(attempts_comparison.annotated_base_image_path, previous_file, force: true)
|
113
|
+
FileUtils.mv(attempts_comparison.reporter.annotated_base_image_path, previous_file, force: true)
|
88
114
|
else
|
89
115
|
warn "[capybara-screenshot-diff] Some attempts was stable, but mistakenly marked as not: " \
|
90
|
-
|
116
|
+
"#{previous_file} and #{file_name} are equal"
|
91
117
|
end
|
92
118
|
|
93
|
-
FileUtils.rm(attempts_comparison.annotated_image_path, force: true)
|
119
|
+
FileUtils.rm(attempts_comparison.reporter.annotated_image_path, force: true)
|
94
120
|
end
|
95
121
|
|
96
122
|
previous_file = file_name
|
97
123
|
end
|
98
124
|
end
|
99
|
-
|
100
|
-
def timeout?(elapsed_time)
|
101
|
-
elapsed_time > wait
|
102
|
-
end
|
103
125
|
end
|
104
126
|
end
|
105
127
|
end
|