capybara-screenshot-diff 1.8.3 → 1.9.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Rakefile +1 -11
- data/capybara-screenshot-diff.gemspec +2 -3
- data/gems.rb +4 -4
- 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 +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 +36 -74
- data/lib/capybara/screenshot/diff/screenshoter.rb +46 -54
- data/lib/capybara/screenshot/diff/stable_screenshoter.rb +65 -63
- data/lib/capybara/screenshot/diff/test_methods.rb +76 -9
- data/lib/capybara/screenshot/diff/{drivers/utils.rb → utils.rb} +0 -7
- data/lib/capybara/screenshot/diff/vcs.rb +25 -23
- 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 +45 -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 -39
- 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
|
@@ -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,13 +78,15 @@ 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
|
-
if (
|
122
|
-
raise
|
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}"
|
123
90
|
end
|
124
91
|
|
125
92
|
sleep 0.025
|
@@ -128,6 +95,31 @@ module Capybara
|
|
128
95
|
|
129
96
|
private
|
130
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
|
+
|
131
123
|
def selenium_with_retina_screen?
|
132
124
|
Os::ON_MAC && BrowserHelpers.selenium? && Screenshot.window_size
|
133
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
|